diff --git a/android/.editorconfig b/android/.editorconfig new file mode 100644 index 0000000000..9a932ce092 --- /dev/null +++ b/android/.editorconfig @@ -0,0 +1,10 @@ +# top-most EditorConfig file +root = true + +; applies to all files +[*] +indent_style = space + +[*.{java, sh, css, html, xml}] +indent_size = 2 + diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000000..43b2d9f989 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,34 @@ +# Covers JetBrains IDE: Android Studio +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# Built application files +**/build/ + +# Local configuration file (sdk path, etc) +**/local.properties + +# User-specific stuff: +**/.idea/libraries/ +**/.idea/codeStyleSettings.xml +**/.idea/dictionaries +**/.idea/workspace.xml +**/.idea/gradle.xml +**/.idea/tasks.xml +**/.idea/misc.xml +**/.idea/modules.xml +**/.idea/vcs.xml +**/*.iml + +# Gradle generated files +**/.gradle/ + +# OS-specific files +**/.DS_Store + +# This is the file output from KMEA; it's part of our build process. +**/libs/keyman-engine.aar + +# keymanweb dependencies +KMEA/app/src/main/assets/ +KMAPro/assets/keyman.js + diff --git a/android/KMAPro/.classpath b/android/KMAPro/.classpath new file mode 100644 index 0000000000..7bc01d9a9c --- /dev/null +++ b/android/KMAPro/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/KMAPro/.idea/.name b/android/KMAPro/.idea/.name new file mode 100644 index 0000000000..c88ba59f98 --- /dev/null +++ b/android/KMAPro/.idea/.name @@ -0,0 +1 @@ +KMAPro \ No newline at end of file diff --git a/android/KMAPro/.idea/compiler.xml b/android/KMAPro/.idea/compiler.xml new file mode 100644 index 0000000000..96cc43efa6 --- /dev/null +++ b/android/KMAPro/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/.idea/copyright/profiles_settings.xml b/android/KMAPro/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000000..e7bedf3377 --- /dev/null +++ b/android/KMAPro/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/android/KMAPro/.idea/encodings.xml b/android/KMAPro/.idea/encodings.xml new file mode 100644 index 0000000000..97626ba454 --- /dev/null +++ b/android/KMAPro/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/KMAPro/.idea/runConfigurations.xml b/android/KMAPro/.idea/runConfigurations.xml new file mode 100644 index 0000000000..7f68460d8b --- /dev/null +++ b/android/KMAPro/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/android/KMAPro/AndroidManifest.xml b/android/KMAPro/AndroidManifest.xml new file mode 100644 index 0000000000..d766b4602c --- /dev/null +++ b/android/KMAPro/AndroidManifest.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/bin/AndroidManifest.xml b/android/KMAPro/bin/AndroidManifest.xml new file mode 100644 index 0000000000..7085034d4c --- /dev/null +++ b/android/KMAPro/bin/AndroidManifest.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/KMAPro/bin/R.txt b/android/KMAPro/bin/R.txt new file mode 100644 index 0000000000..4f8d5b58a3 --- /dev/null +++ b/android/KMAPro/bin/R.txt @@ -0,0 +1,330 @@ +int anim fade_in 0x7f040000 +int anim fade_out 0x7f040001 +int anim hold 0x7f040002 +int anim key_preview_fade_in 0x7f040003 +int anim key_preview_fade_out 0x7f040004 +int attr confirm_logout 0x7f01000c +int attr done_button_background 0x7f010006 +int attr done_button_text 0x7f010004 +int attr extra_fields 0x7f010001 +int attr fetch_user_info 0x7f01000d +int attr is_cropped 0x7f010011 +int attr login_text 0x7f01000e +int attr logout_text 0x7f01000f +int attr multi_select 0x7f010007 +int attr preset_size 0x7f010010 +int attr radius_in_meters 0x7f010008 +int attr results_limit 0x7f010009 +int attr search_text 0x7f01000a +int attr show_pictures 0x7f010000 +int attr show_search_box 0x7f01000b +int attr show_title_bar 0x7f010002 +int attr title_bar_background 0x7f010005 +int attr title_text 0x7f010003 +int color activated_bg 0x7f080007 +int color com_facebook_blue 0x7f080002 +int color com_facebook_loginview_text_color 0x7f080006 +int color com_facebook_picker_search_bar_background 0x7f080000 +int color com_facebook_picker_search_bar_text 0x7f080001 +int color com_facebook_usersettingsfragment_connected_shadow_color 0x7f080004 +int color com_facebook_usersettingsfragment_connected_text_color 0x7f080003 +int color com_facebook_usersettingsfragment_not_connected_text_color 0x7f080005 +int color key_preview_bg 0x7f08000b +int color key_preview_bg2 0x7f08000c +int color key_preview_border 0x7f08000d +int color keyman_blue 0x7f080011 +int color keyman_orange 0x7f08000f +int color keyman_red 0x7f080010 +int color popup_bg 0x7f080008 +int color popup_bg2 0x7f080009 +int color popup_border 0x7f08000a +int color yellow_background 0x7f08000e +int dimen activity_horizontal_margin 0x7f07001a +int dimen activity_vertical_margin 0x7f07001b +int dimen com_facebook_loginview_compound_drawable_padding 0x7f070008 +int dimen com_facebook_loginview_padding_bottom 0x7f070007 +int dimen com_facebook_loginview_padding_left 0x7f070004 +int dimen com_facebook_loginview_padding_right 0x7f070005 +int dimen com_facebook_loginview_padding_top 0x7f070006 +int dimen com_facebook_loginview_text_size 0x7f070009 +int dimen com_facebook_picker_divider_width 0x7f070001 +int dimen com_facebook_picker_place_image_size 0x7f070000 +int dimen com_facebook_profilepictureview_preset_size_large 0x7f07000c +int dimen com_facebook_profilepictureview_preset_size_normal 0x7f07000b +int dimen com_facebook_profilepictureview_preset_size_small 0x7f07000a +int dimen com_facebook_tooltip_horizontal_padding 0x7f07000d +int dimen com_facebook_usersettingsfragment_profile_picture_height 0x7f070003 +int dimen com_facebook_usersettingsfragment_profile_picture_width 0x7f070002 +int dimen help_bubble_height 0x7f070017 +int dimen help_bubble_offset_x 0x7f070018 +int dimen help_bubble_offset_y 0x7f070019 +int dimen help_bubble_width 0x7f070016 +int dimen key_height 0x7f070010 +int dimen key_width 0x7f07000f +int dimen keyboard_height 0x7f07000e +int dimen popup_arrow_height 0x7f070012 +int dimen popup_arrow_width 0x7f070011 +int dimen popup_margin 0x7f070013 +int dimen popup_offset_y 0x7f070015 +int dimen popup_padding 0x7f070014 +int drawable com_facebook_button_blue 0x7f020000 +int drawable com_facebook_button_blue_focused 0x7f020001 +int drawable com_facebook_button_blue_normal 0x7f020002 +int drawable com_facebook_button_blue_pressed 0x7f020003 +int drawable com_facebook_button_check 0x7f020004 +int drawable com_facebook_button_check_off 0x7f020005 +int drawable com_facebook_button_check_on 0x7f020006 +int drawable com_facebook_button_grey_focused 0x7f020007 +int drawable com_facebook_button_grey_normal 0x7f020008 +int drawable com_facebook_button_grey_pressed 0x7f020009 +int drawable com_facebook_close 0x7f02000a +int drawable com_facebook_inverse_icon 0x7f02000b +int drawable com_facebook_list_divider 0x7f02000c +int drawable com_facebook_list_section_header_background 0x7f02000d +int drawable com_facebook_loginbutton_silver 0x7f02000e +int drawable com_facebook_logo 0x7f02000f +int drawable com_facebook_picker_default_separator_color 0x7f02003e +int drawable com_facebook_picker_item_background 0x7f020010 +int drawable com_facebook_picker_list_focused 0x7f020011 +int drawable com_facebook_picker_list_longpressed 0x7f020012 +int drawable com_facebook_picker_list_pressed 0x7f020013 +int drawable com_facebook_picker_list_selector 0x7f020014 +int drawable com_facebook_picker_list_selector_background_transition 0x7f020015 +int drawable com_facebook_picker_list_selector_disabled 0x7f020016 +int drawable com_facebook_picker_magnifier 0x7f020017 +int drawable com_facebook_picker_top_button 0x7f020018 +int drawable com_facebook_place_default_icon 0x7f020019 +int drawable com_facebook_profile_default_icon 0x7f02001a +int drawable com_facebook_profile_picture_blank_portrait 0x7f02001b +int drawable com_facebook_profile_picture_blank_square 0x7f02001c +int drawable com_facebook_tooltip_black_background 0x7f02001d +int drawable com_facebook_tooltip_black_bottomnub 0x7f02001e +int drawable com_facebook_tooltip_black_topnub 0x7f02001f +int drawable com_facebook_tooltip_black_xout 0x7f020020 +int drawable com_facebook_tooltip_blue_background 0x7f020021 +int drawable com_facebook_tooltip_blue_bottomnub 0x7f020022 +int drawable com_facebook_tooltip_blue_topnub 0x7f020023 +int drawable com_facebook_tooltip_blue_xout 0x7f020024 +int drawable com_facebook_top_background 0x7f020025 +int drawable com_facebook_top_button 0x7f020026 +int drawable com_facebook_usersettingsfragment_background_gradient 0x7f020027 +int drawable ic_action_accept 0x7f020028 +int drawable ic_action_back 0x7f020029 +int drawable ic_action_get_started 0x7f02002a +int drawable ic_action_next 0x7f02002b +int drawable ic_action_no1 0x7f02002c +int drawable ic_action_no2 0x7f02002d +int drawable ic_facebook_logo 0x7f02002e +int drawable ic_launcher 0x7f02002f +int drawable ic_light_action_info 0x7f020030 +int drawable ic_light_action_overflow 0x7f020031 +int drawable ic_light_action_share 0x7f020032 +int drawable ic_light_action_textsize 0x7f020033 +int drawable ic_light_action_trash 0x7f020034 +int drawable ic_light_dialog_textsize_down 0x7f020035 +int drawable ic_light_dialog_textsize_up 0x7f020036 +int drawable keyman_logo 0x7f020037 +int drawable list_header 0x7f020038 +int drawable list_item 0x7f020039 +int drawable oval_button 0x7f02003a +int drawable subkey_phone 0x7f02003b +int drawable subkey_tablet 0x7f02003c +int drawable textview_bg 0x7f02003d +int id action_clear_text 0x7f06003c +int id action_get_started 0x7f06003e +int id action_info 0x7f06003d +int id action_overflow 0x7f06003b +int id action_share 0x7f060039 +int id action_text_size 0x7f06003a +int id bar_title 0x7f060031 +int id button1 0x7f060033 +int id checkBox1 0x7f060023 +int id close_button 0x7f060027 +int id com_facebook_body_frame 0x7f06001a +int id com_facebook_button_xout 0x7f06001c +int id com_facebook_login_activity_progress_bar 0x7f06000a +int id com_facebook_picker_activity_circle 0x7f060009 +int id com_facebook_picker_checkbox 0x7f06000c +int id com_facebook_picker_checkbox_stub 0x7f060010 +int id com_facebook_picker_divider 0x7f060014 +int id com_facebook_picker_done_button 0x7f060013 +int id com_facebook_picker_image 0x7f06000d +int id com_facebook_picker_list_section_header 0x7f060011 +int id com_facebook_picker_list_view 0x7f060008 +int id com_facebook_picker_profile_pic_stub 0x7f06000e +int id com_facebook_picker_row_activity_circle 0x7f06000b +int id com_facebook_picker_search_text 0x7f060019 +int id com_facebook_picker_title 0x7f06000f +int id com_facebook_picker_title_bar 0x7f060016 +int id com_facebook_picker_title_bar_stub 0x7f060015 +int id com_facebook_picker_top_bar 0x7f060012 +int id com_facebook_search_bar_view 0x7f060018 +int id com_facebook_tooltip_bubble_view_bottom_pointer 0x7f06001e +int id com_facebook_tooltip_bubble_view_text_body 0x7f06001d +int id com_facebook_tooltip_bubble_view_top_pointer 0x7f06001b +int id com_facebook_usersettingsfragment_login_button 0x7f060021 +int id com_facebook_usersettingsfragment_logo_image 0x7f06001f +int id com_facebook_usersettingsfragment_profile_name 0x7f060020 +int id grid 0x7f060035 +int id image1 0x7f06002c +int id imageButton1 0x7f06002f +int id kmKeyPreviewView 0x7f06002a +int id kmPopoverView 0x7f060028 +int id kmTextView1 0x7f060007 +int id large 0x7f060002 +int id left_button 0x7f060030 +int id left_icon 0x7f060024 +int id linearLayout1 0x7f06002e +int id listView 0x7f060022 +int id loginButton 0x7f060004 +int id menu_title 0x7f060026 +int id normal 0x7f060001 +int id picker_subtitle 0x7f060017 +int id popup_delete 0x7f06003f +int id right_button 0x7f060032 +int id seekBar 0x7f060037 +int id shareButton 0x7f060005 +int id small 0x7f060000 +int id subkey_button 0x7f060034 +int id text 0x7f060025 +int id text1 0x7f06002b +int id text2 0x7f06002d +int id textSizeDownButton 0x7f060036 +int id textSizeUpButton 0x7f060038 +int id textView 0x7f060003 +int id textView1 0x7f060029 +int id webView1 0x7f060006 +int layout activity_fbshare 0x7f030000 +int layout activity_info 0x7f030001 +int layout activity_main 0x7f030002 +int layout com_facebook_friendpickerfragment 0x7f030003 +int layout com_facebook_login_activity_layout 0x7f030004 +int layout com_facebook_picker_activity_circle_row 0x7f030005 +int layout com_facebook_picker_checkbox 0x7f030006 +int layout com_facebook_picker_image 0x7f030007 +int layout com_facebook_picker_list_row 0x7f030008 +int layout com_facebook_picker_list_section_header 0x7f030009 +int layout com_facebook_picker_search_box 0x7f03000a +int layout com_facebook_picker_title_bar 0x7f03000b +int layout com_facebook_picker_title_bar_stub 0x7f03000c +int layout com_facebook_placepickerfragment 0x7f03000d +int layout com_facebook_placepickerfragment_list_row 0x7f03000e +int layout com_facebook_search_bar_layout 0x7f03000f +int layout com_facebook_tooltip_bubble 0x7f030010 +int layout com_facebook_usersettingsfragment 0x7f030011 +int layout get_started_list_layout 0x7f030012 +int layout get_started_row_layout 0x7f030013 +int layout get_started_title_layout 0x7f030014 +int layout help_bubble_layout 0x7f030015 +int layout key_preview_layout 0x7f030016 +int layout list_layout 0x7f030017 +int layout list_row_layout1 0x7f030018 +int layout list_row_layout2 0x7f030019 +int layout list_row_layout3 0x7f03001a +int layout list_title_layout1 0x7f03001b +int layout list_title_layout2 0x7f03001c +int layout overlay_layout 0x7f03001d +int layout subkey_layout 0x7f03001e +int layout subkeys_popup_layout 0x7f03001f +int layout text_size_controller 0x7f030020 +int menu main 0x7f0b0000 +int menu popup 0x7f0b0001 +int string action_clear_text 0x7f09002e +int string action_info 0x7f09002f +int string action_overflow 0x7f09002d +int string action_settings 0x7f09001c +int string action_share 0x7f09002b +int string action_text_size 0x7f09002c +int string allow 0x7f09003a +int string app_id 0x7f09002a +int string app_name 0x7f09001b +int string application_error 0x7f09003b +int string buy_button 0x7f09003f +int string check_license 0x7f090037 +int string checking_license 0x7f090038 +int string com_facebook_choose_friends 0x7f09000f +int string com_facebook_dialogloginactivity_ok_button 0x7f090000 +int string com_facebook_internet_permission_error_message 0x7f090013 +int string com_facebook_internet_permission_error_title 0x7f090012 +int string com_facebook_loading 0x7f090011 +int string com_facebook_loginview_cancel_action 0x7f090006 +int string com_facebook_loginview_log_in_button 0x7f090002 +int string com_facebook_loginview_log_out_action 0x7f090005 +int string com_facebook_loginview_log_out_button 0x7f090001 +int string com_facebook_loginview_logged_in_as 0x7f090003 +int string com_facebook_loginview_logged_in_using_facebook 0x7f090004 +int string com_facebook_logo_content_description 0x7f090007 +int string com_facebook_nearby 0x7f090010 +int string com_facebook_picker_done_button_text 0x7f09000e +int string com_facebook_placepicker_subtitle_catetory_only_format 0x7f09000c +int string com_facebook_placepicker_subtitle_format 0x7f09000b +int string com_facebook_placepicker_subtitle_were_here_only_format 0x7f09000d +int string com_facebook_requesterror_password_changed 0x7f090016 +int string com_facebook_requesterror_permissions 0x7f090018 +int string com_facebook_requesterror_reconnect 0x7f090017 +int string com_facebook_requesterror_relogin 0x7f090015 +int string com_facebook_requesterror_web_login 0x7f090014 +int string com_facebook_tooltip_default 0x7f090019 +int string com_facebook_usersettingsfragment_log_in_button 0x7f090008 +int string com_facebook_usersettingsfragment_logged_in 0x7f090009 +int string com_facebook_usersettingsfragment_not_logged_in 0x7f09000a +int string device_type 0x7f09001a +int string dont_allow 0x7f090039 +int string dont_show_again 0x7f090036 +int string get_started 0x7f090035 +int string help_bubble_text 0x7f090027 +int string ic_text_size_down 0x7f090031 +int string ic_text_size_up 0x7f090030 +int string image_button 0x7f090023 +int string image_view 0x7f090024 +int string kma_prefs_name 0x7f090026 +int string label_action_button 0x7f09001f +int string label_add 0x7f090021 +int string label_back 0x7f090022 +int string label_done 0x7f090020 +int string popup_menu_delete 0x7f090025 +int string quit_button 0x7f090041 +int string reactivate 0x7f090029 +int string retry_button 0x7f090040 +int string share 0x7f090034 +int string textview_hint 0x7f090033 +int string title_add_keyboard 0x7f09001e +int string title_keyboards 0x7f09001d +int string top_bar_line 0x7f090032 +int string trial_expired 0x7f090028 +int string unlicensed_dialog_body 0x7f09003d +int string unlicensed_dialog_retry_body 0x7f09003e +int string unlicensed_dialog_title 0x7f09003c +int style AppBaseTheme 0x7f0a0003 +int style AppTheme 0x7f0a0004 +int style KeyPreviewAnim 0x7f0a0006 +int style KeymanActionBar 0x7f0a0008 +int style PopupAnim 0x7f0a0005 +int style com_facebook_loginview_default_style 0x7f0a0000 +int style com_facebook_loginview_silver_style 0x7f0a0001 +int style subkey_button 0x7f0a0007 +int style tooltip_bubble_text 0x7f0a0002 +int[] styleable com_facebook_friend_picker_fragment { 0x7f010007 } +int styleable com_facebook_friend_picker_fragment_multi_select 0 +int[] styleable com_facebook_login_view { 0x7f01000c, 0x7f01000d, 0x7f01000e, 0x7f01000f } +int styleable com_facebook_login_view_confirm_logout 0 +int styleable com_facebook_login_view_fetch_user_info 1 +int styleable com_facebook_login_view_login_text 2 +int styleable com_facebook_login_view_logout_text 3 +int[] styleable com_facebook_picker_fragment { 0x7f010000, 0x7f010001, 0x7f010002, 0x7f010003, 0x7f010004, 0x7f010005, 0x7f010006 } +int styleable com_facebook_picker_fragment_done_button_background 6 +int styleable com_facebook_picker_fragment_done_button_text 4 +int styleable com_facebook_picker_fragment_extra_fields 1 +int styleable com_facebook_picker_fragment_show_pictures 0 +int styleable com_facebook_picker_fragment_show_title_bar 2 +int styleable com_facebook_picker_fragment_title_bar_background 5 +int styleable com_facebook_picker_fragment_title_text 3 +int[] styleable com_facebook_place_picker_fragment { 0x7f010008, 0x7f010009, 0x7f01000a, 0x7f01000b } +int styleable com_facebook_place_picker_fragment_radius_in_meters 0 +int styleable com_facebook_place_picker_fragment_results_limit 1 +int styleable com_facebook_place_picker_fragment_search_text 2 +int styleable com_facebook_place_picker_fragment_show_search_box 3 +int[] styleable com_facebook_profile_picture_view { 0x7f010010, 0x7f010011 } +int styleable com_facebook_profile_picture_view_is_cropped 1 +int styleable com_facebook_profile_picture_view_preset_size 0 +int xml method 0x7f050000 diff --git a/android/KMAPro/bin/classes.dex b/android/KMAPro/bin/classes.dex new file mode 100644 index 0000000000..c1119316fd Binary files /dev/null and b/android/KMAPro/bin/classes.dex differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$attr.class b/android/KMAPro/bin/classes/com/facebook/android/R$attr.class new file mode 100644 index 0000000000..66e1dbae86 Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$attr.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$color.class b/android/KMAPro/bin/classes/com/facebook/android/R$color.class new file mode 100644 index 0000000000..f0f0a525e6 Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$color.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$dimen.class b/android/KMAPro/bin/classes/com/facebook/android/R$dimen.class new file mode 100644 index 0000000000..12505a0f7c Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$dimen.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$drawable.class b/android/KMAPro/bin/classes/com/facebook/android/R$drawable.class new file mode 100644 index 0000000000..35a15545ae Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$drawable.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$id.class b/android/KMAPro/bin/classes/com/facebook/android/R$id.class new file mode 100644 index 0000000000..9c04fe2863 Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$id.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$layout.class b/android/KMAPro/bin/classes/com/facebook/android/R$layout.class new file mode 100644 index 0000000000..bd7d3992df Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$layout.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$string.class b/android/KMAPro/bin/classes/com/facebook/android/R$string.class new file mode 100644 index 0000000000..d678cd9ce6 Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$string.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$style.class b/android/KMAPro/bin/classes/com/facebook/android/R$style.class new file mode 100644 index 0000000000..6f006b877b Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$style.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R$styleable.class b/android/KMAPro/bin/classes/com/facebook/android/R$styleable.class new file mode 100644 index 0000000000..5087fa315f Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R$styleable.class differ diff --git a/android/KMAPro/bin/classes/com/facebook/android/R.class b/android/KMAPro/bin/classes/com/facebook/android/R.class new file mode 100644 index 0000000000..15b0804020 Binary files /dev/null and b/android/KMAPro/bin/classes/com/facebook/android/R.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/AESObfuscator.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/AESObfuscator.class new file mode 100644 index 0000000000..58b9af11c3 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/AESObfuscator.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/APKExpansionPolicy.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/APKExpansionPolicy.class new file mode 100644 index 0000000000..ecd37dd04b Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/APKExpansionPolicy.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/DeviceLimiter.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/DeviceLimiter.class new file mode 100644 index 0000000000..b192b9269c Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/DeviceLimiter.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener$Stub$Proxy.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener$Stub$Proxy.class new file mode 100644 index 0000000000..ed2b4d149a Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener$Stub$Proxy.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener$Stub.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener$Stub.class new file mode 100644 index 0000000000..10e1ec791d Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener$Stub.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener.class new file mode 100644 index 0000000000..1e262529e2 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicenseResultListener.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService$Stub$Proxy.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService$Stub$Proxy.class new file mode 100644 index 0000000000..83bb8e6623 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService$Stub$Proxy.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService$Stub.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService$Stub.class new file mode 100644 index 0000000000..d1838e1dcd Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService$Stub.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService.class new file mode 100644 index 0000000000..0fbab9b2fc Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ILicensingService.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener$1.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener$1.class new file mode 100644 index 0000000000..399cec2f18 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener$1.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener$2.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener$2.class new file mode 100644 index 0000000000..b06dbef352 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener$2.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener.class new file mode 100644 index 0000000000..49fb5d7f05 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker$ResultListener.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker.class new file mode 100644 index 0000000000..190fde3915 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseChecker.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseCheckerCallback.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseCheckerCallback.class new file mode 100644 index 0000000000..026dc87d4b Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseCheckerCallback.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseValidator.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseValidator.class new file mode 100644 index 0000000000..1bc77eac8a Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/LicenseValidator.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/NullDeviceLimiter.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/NullDeviceLimiter.class new file mode 100644 index 0000000000..80ca5856cc Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/NullDeviceLimiter.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/Obfuscator.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/Obfuscator.class new file mode 100644 index 0000000000..1b2e2ecb2c Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/Obfuscator.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/Policy.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/Policy.class new file mode 100644 index 0000000000..cdc88ee15f Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/Policy.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/PreferenceObfuscator.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/PreferenceObfuscator.class new file mode 100644 index 0000000000..4f75c8fb27 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/PreferenceObfuscator.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ResponseData.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ResponseData.class new file mode 100644 index 0000000000..cdd40cbb40 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ResponseData.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ServerManagedPolicy.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ServerManagedPolicy.class new file mode 100644 index 0000000000..a3fd0bf7c0 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ServerManagedPolicy.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/StrictPolicy.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/StrictPolicy.class new file mode 100644 index 0000000000..07d4190dff Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/StrictPolicy.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/ValidationException.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ValidationException.class new file mode 100644 index 0000000000..6da11d88e1 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/ValidationException.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/util/Base64.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/util/Base64.class new file mode 100644 index 0000000000..1dad63ee3a Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/util/Base64.class differ diff --git a/android/KMAPro/bin/classes/com/google/android/vending/licensing/util/Base64DecoderException.class b/android/KMAPro/bin/classes/com/google/android/vending/licensing/util/Base64DecoderException.class new file mode 100644 index 0000000000..89d3a9f249 Binary files /dev/null and b/android/KMAPro/bin/classes/com/google/android/vending/licensing/util/Base64DecoderException.class differ diff --git a/android/KMAPro/bin/classes/com/keyman/android/SystemKeyboard.class b/android/KMAPro/bin/classes/com/keyman/android/SystemKeyboard.class new file mode 100644 index 0000000000..707d9e639a Binary files /dev/null and b/android/KMAPro/bin/classes/com/keyman/android/SystemKeyboard.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/BuildConfig.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/BuildConfig.class new file mode 100644 index 0000000000..49b769cef3 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/BuildConfig.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareActivity.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareActivity.class new file mode 100644 index 0000000000..b05096840c Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareActivity.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$1.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$1.class new file mode 100644 index 0000000000..27de4ef777 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$1.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$2.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$2.class new file mode 100644 index 0000000000..65e8350950 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$2.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$3.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$3.class new file mode 100644 index 0000000000..1f1fb4edf7 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment$3.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment.class new file mode 100644 index 0000000000..6fb5078e90 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/FBShareFragment.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$1.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$1.class new file mode 100644 index 0000000000..d71b6c903e Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$1.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$2.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$2.class new file mode 100644 index 0000000000..b5b4007014 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$2.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$3.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$3.class new file mode 100644 index 0000000000..3d3359b228 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity$3.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity.class new file mode 100644 index 0000000000..751ae0392f Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/GetStartedActivity.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/InfoActivity$1.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/InfoActivity$1.class new file mode 100644 index 0000000000..9af2ace80b Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/InfoActivity$1.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/InfoActivity.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/InfoActivity.class new file mode 100644 index 0000000000..81ead6e50c Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/InfoActivity.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/KMListAdapter.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/KMListAdapter.class new file mode 100644 index 0000000000..3c3bfa8952 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/KMListAdapter.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$1.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$1.class new file mode 100644 index 0000000000..9222694ec7 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$1.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$2.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$2.class new file mode 100644 index 0000000000..4be253fa5f Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$2.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$3.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$3.class new file mode 100644 index 0000000000..1fa0501693 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$3.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$4.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$4.class new file mode 100644 index 0000000000..cf83d12529 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$4.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$KMLicenseCheckerCallback.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$KMLicenseCheckerCallback.class new file mode 100644 index 0000000000..bcdf19e3fe Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity$KMLicenseCheckerCallback.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity.class new file mode 100644 index 0000000000..ccd3a964c1 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/LicenseCheckActivity.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$1.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$1.class new file mode 100644 index 0000000000..ead8922d89 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$1.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$10.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$10.class new file mode 100644 index 0000000000..2aef2e4c48 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$10.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$2.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$2.class new file mode 100644 index 0000000000..e4898ce540 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$2.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$3.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$3.class new file mode 100644 index 0000000000..1d4eea3d45 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$3.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$4.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$4.class new file mode 100644 index 0000000000..d9090b5537 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$4.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$5.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$5.class new file mode 100644 index 0000000000..4a4f90f558 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$5.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$6.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$6.class new file mode 100644 index 0000000000..b778d4823c Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$6.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$7.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$7.class new file mode 100644 index 0000000000..0cc76c5ff9 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$7.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$8.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$8.class new file mode 100644 index 0000000000..f1654f6eb1 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$8.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$9.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$9.class new file mode 100644 index 0000000000..90add3bae7 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity$9.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity.class new file mode 100644 index 0000000000..fdec434475 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/MainActivity.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/NetworkStateReceiver$OnNetworkStateChangeListener.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/NetworkStateReceiver$OnNetworkStateChangeListener.class new file mode 100644 index 0000000000..4b00c3594f Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/NetworkStateReceiver$OnNetworkStateChangeListener.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/NetworkStateReceiver.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/NetworkStateReceiver.class new file mode 100644 index 0000000000..8d9e2f1b95 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/NetworkStateReceiver.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$anim.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$anim.class new file mode 100644 index 0000000000..04f6331e9e Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$anim.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$attr.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$attr.class new file mode 100644 index 0000000000..776389deae Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$attr.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$color.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$color.class new file mode 100644 index 0000000000..0cd9c7e74b Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$color.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$dimen.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$dimen.class new file mode 100644 index 0000000000..f7bcd7a6b4 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$dimen.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$drawable.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$drawable.class new file mode 100644 index 0000000000..e9cb3da77d Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$drawable.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$id.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$id.class new file mode 100644 index 0000000000..2cfd0d0e49 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$id.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$layout.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$layout.class new file mode 100644 index 0000000000..bc2c756896 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$layout.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$menu.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$menu.class new file mode 100644 index 0000000000..1aa3dc81d4 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$menu.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$string.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$string.class new file mode 100644 index 0000000000..857af26a44 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$string.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$style.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$style.class new file mode 100644 index 0000000000..05d0b708b8 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$style.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$styleable.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$styleable.class new file mode 100644 index 0000000000..9c65b6a411 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$styleable.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$xml.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$xml.class new file mode 100644 index 0000000000..fa2d01df10 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R$xml.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R.class b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R.class new file mode 100644 index 0000000000..4fc9a77701 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmapro/R.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$anim.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$anim.class new file mode 100644 index 0000000000..d710f9b66e Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$anim.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$color.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$color.class new file mode 100644 index 0000000000..b427c4132e Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$color.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$dimen.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$dimen.class new file mode 100644 index 0000000000..46ae5638e1 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$dimen.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$drawable.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$drawable.class new file mode 100644 index 0000000000..3f3207f9b2 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$drawable.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$id.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$id.class new file mode 100644 index 0000000000..a3f84308e8 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$id.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$layout.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$layout.class new file mode 100644 index 0000000000..3929e204a2 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$layout.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$menu.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$menu.class new file mode 100644 index 0000000000..3ed88f44ec Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$menu.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$string.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$string.class new file mode 100644 index 0000000000..bf0172d36e Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$string.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$style.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$style.class new file mode 100644 index 0000000000..e78f1394e6 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R$style.class differ diff --git a/android/KMAPro/bin/classes/com/tavultesoft/kmea/R.class b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R.class new file mode 100644 index 0000000000..d6837a6a22 Binary files /dev/null and b/android/KMAPro/bin/classes/com/tavultesoft/kmea/R.class differ diff --git a/android/KMAPro/bin/dexedLibs/android-support-v4-991d4cd23fda4823f4d43f5d15daf3e5.jar b/android/KMAPro/bin/dexedLibs/android-support-v4-991d4cd23fda4823f4d43f5d15daf3e5.jar new file mode 100644 index 0000000000..88c651ffbc Binary files /dev/null and b/android/KMAPro/bin/dexedLibs/android-support-v4-991d4cd23fda4823f4d43f5d15daf3e5.jar differ diff --git a/android/KMAPro/bin/dexedLibs/bolts-5ffbc13154aa49efd571eae0368e6664.jar b/android/KMAPro/bin/dexedLibs/bolts-5ffbc13154aa49efd571eae0368e6664.jar new file mode 100644 index 0000000000..b27cf2d6e8 Binary files /dev/null and b/android/KMAPro/bin/dexedLibs/bolts-5ffbc13154aa49efd571eae0368e6664.jar differ diff --git a/android/KMAPro/bin/dexedLibs/facebooksdk-98f2d9051427b41a2f4112d87fd739ec.jar b/android/KMAPro/bin/dexedLibs/facebooksdk-98f2d9051427b41a2f4112d87fd739ec.jar new file mode 100644 index 0000000000..d34ae9ecfb Binary files /dev/null and b/android/KMAPro/bin/dexedLibs/facebooksdk-98f2d9051427b41a2f4112d87fd739ec.jar differ diff --git a/android/KMAPro/bin/dexedLibs/kmea-8dd330b4a5a76cba469f7e28a23f939a.jar b/android/KMAPro/bin/dexedLibs/kmea-8dd330b4a5a76cba469f7e28a23f939a.jar new file mode 100644 index 0000000000..fd6b2f4632 Binary files /dev/null and b/android/KMAPro/bin/dexedLibs/kmea-8dd330b4a5a76cba469f7e28a23f939a.jar differ diff --git a/android/KMAPro/bin/jarlist.cache b/android/KMAPro/bin/jarlist.cache new file mode 100644 index 0000000000..9a6c31d644 --- /dev/null +++ b/android/KMAPro/bin/jarlist.cache @@ -0,0 +1,6 @@ +# cache for current jar dependency. DO NOT EDIT. +# format is +# Encoding is UTF-8 +1402548810000 621451 5896b0a4e377ac4242eb2bc785220c1c4fc052f4 /Users/serkankurt/Desktop/facebook-android-sdk-3.15.0/facebook/libs/android-support-v4.jar +1387234026000 621451 5896b0a4e377ac4242eb2bc785220c1c4fc052f4 /Users/serkankurt/Documents/workspace/KMEA/libs/android-support-v4.jar +1387235477000 621451 5896b0a4e377ac4242eb2bc785220c1c4fc052f4 /Users/serkankurt/Documents/workspace/KMAPro/libs/android-support-v4.jar diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_get_started.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_get_started.png new file mode 100644 index 0000000000..73ecb166ff Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_get_started.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_no1.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_no1.png new file mode 100644 index 0000000000..fe17843f08 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_no1.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_no2.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_no2.png new file mode 100644 index 0000000000..d58704aa4e Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_action_no2.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_facebook_logo.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_facebook_logo.png new file mode 100644 index 0000000000..386211190f Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_facebook_logo.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_launcher.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000..466d0007a4 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_launcher.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_info.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_info.png new file mode 100644 index 0000000000..326da27285 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_info.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_overflow.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_overflow.png new file mode 100644 index 0000000000..b866e933fe Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_overflow.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_share.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_share.png new file mode 100644 index 0000000000..40b8b33a98 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_share.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_textsize.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_textsize.png new file mode 100644 index 0000000000..c55cabf5d5 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_trash.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_trash.png new file mode 100644 index 0000000000..b573993464 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_action_trash.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_dialog_textsize_down.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_dialog_textsize_down.png new file mode 100644 index 0000000000..97c334817a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_dialog_textsize_down.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_dialog_textsize_up.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_dialog_textsize_up.png new file mode 100644 index 0000000000..a76c5e04c9 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/ic_light_dialog_textsize_up.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-hdpi/keyman_logo.9.png b/android/KMAPro/bin/res/crunch/drawable-hdpi/keyman_logo.9.png new file mode 100644 index 0000000000..6f4c680a93 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-hdpi/keyman_logo.9.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_get_started.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_get_started.png new file mode 100644 index 0000000000..eb0bf4524c Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_get_started.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_no1.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_no1.png new file mode 100644 index 0000000000..2f5d81b545 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_no1.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_no2.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_no2.png new file mode 100644 index 0000000000..75e66cbb90 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_action_no2.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_facebook_logo.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_facebook_logo.png new file mode 100644 index 0000000000..5632504ede Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_facebook_logo.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_launcher.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000..02beba7aef Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_launcher.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_info.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_info.png new file mode 100644 index 0000000000..a2f53b2487 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_info.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_overflow.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_overflow.png new file mode 100644 index 0000000000..aad4dd529a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_overflow.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_share.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_share.png new file mode 100644 index 0000000000..a7aa9e86d7 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_share.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_textsize.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_textsize.png new file mode 100644 index 0000000000..a9bdc6bdf0 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_trash.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_trash.png new file mode 100644 index 0000000000..826d60b87a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_action_trash.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_dialog_textsize_down.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_dialog_textsize_down.png new file mode 100644 index 0000000000..82dffd2266 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_dialog_textsize_down.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_dialog_textsize_up.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_dialog_textsize_up.png new file mode 100644 index 0000000000..4e3fa3ee3a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/ic_light_dialog_textsize_up.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-mdpi/keyman_logo.9.png b/android/KMAPro/bin/res/crunch/drawable-mdpi/keyman_logo.9.png new file mode 100644 index 0000000000..cff8585f8c Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-mdpi/keyman_logo.9.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_get_started.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_get_started.png new file mode 100644 index 0000000000..14bf299b51 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_get_started.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_no1.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_no1.png new file mode 100644 index 0000000000..5591da62f3 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_no1.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_no2.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_no2.png new file mode 100644 index 0000000000..2f4f3051d1 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_action_no2.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_facebook_logo.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_facebook_logo.png new file mode 100644 index 0000000000..461df7f649 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_facebook_logo.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_launcher.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..23d41f2328 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_launcher.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_info.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_info.png new file mode 100644 index 0000000000..cf3ff64d27 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_info.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_overflow.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_overflow.png new file mode 100644 index 0000000000..76da7d1b6a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_overflow.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_share.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_share.png new file mode 100644 index 0000000000..596533571e Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_share.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_textsize.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_textsize.png new file mode 100644 index 0000000000..db0014d7e4 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_trash.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_trash.png new file mode 100644 index 0000000000..2f8f7cd86b Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_action_trash.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_dialog_textsize_down.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_dialog_textsize_down.png new file mode 100644 index 0000000000..bcc100dd86 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_dialog_textsize_down.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_dialog_textsize_up.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_dialog_textsize_up.png new file mode 100644 index 0000000000..885e97ebc6 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/ic_light_dialog_textsize_up.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xhdpi/keyman_logo.9.png b/android/KMAPro/bin/res/crunch/drawable-xhdpi/keyman_logo.9.png new file mode 100644 index 0000000000..bd4f44d28a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xhdpi/keyman_logo.9.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xlarge-mdpi/keyman_logo.9.png b/android/KMAPro/bin/res/crunch/drawable-xlarge-mdpi/keyman_logo.9.png new file mode 100644 index 0000000000..b10516bbf2 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xlarge-mdpi/keyman_logo.9.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_get_started.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_get_started.png new file mode 100644 index 0000000000..f59d3f846f Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_get_started.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_no1.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_no1.png new file mode 100644 index 0000000000..adb58921b5 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_no1.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_no2.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_no2.png new file mode 100644 index 0000000000..652a3d31dc Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_action_no2.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_facebook_logo.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_facebook_logo.png new file mode 100644 index 0000000000..a13c8a4d3a Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_facebook_logo.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_launcher.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..5a26f06268 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_launcher.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_info.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_info.png new file mode 100644 index 0000000000..8327511875 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_info.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_overflow.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_overflow.png new file mode 100644 index 0000000000..7790e93c96 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_overflow.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_share.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_share.png new file mode 100644 index 0000000000..33977efca0 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_share.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_textsize.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_textsize.png new file mode 100644 index 0000000000..be56ad96ab Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_textsize.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_trash.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_trash.png new file mode 100644 index 0000000000..c5b6ca4cba Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_action_trash.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_dialog_textsize_down.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_dialog_textsize_down.png new file mode 100644 index 0000000000..baa2cbb9d9 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_dialog_textsize_down.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_dialog_textsize_up.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_dialog_textsize_up.png new file mode 100644 index 0000000000..f288e64ae9 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/ic_light_dialog_textsize_up.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxhdpi/keyman_logo.9.png b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/keyman_logo.9.png new file mode 100644 index 0000000000..198440d023 Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxhdpi/keyman_logo.9.png differ diff --git a/android/KMAPro/bin/res/crunch/drawable-xxxhdpi/ic_launcher.png b/android/KMAPro/bin/res/crunch/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..f37b7a48cf Binary files /dev/null and b/android/KMAPro/bin/res/crunch/drawable-xxxhdpi/ic_launcher.png differ diff --git a/android/KMAPro/bin/resources.ap_ b/android/KMAPro/bin/resources.ap_ new file mode 100644 index 0000000000..ae11bc3237 Binary files /dev/null and b/android/KMAPro/bin/resources.ap_ differ diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle new file mode 100644 index 0000000000..7a71440ab8 --- /dev/null +++ b/android/KMAPro/build.gradle @@ -0,0 +1,20 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + jcenter() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.github.triplet.gradle:play-publisher:1.2.0' + } +} + +allprojects { + repositories { + jcenter() + maven { + url "https://maven.google.com" + } + } +} diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh new file mode 100644 index 0000000000..14e8b36022 --- /dev/null +++ b/android/KMAPro/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# Build KMAPro + +echo Build KMAPro +./gradlew clean build diff --git a/android/KMAPro/facebookSDK/build.gradle b/android/KMAPro/facebookSDK/build.gradle new file mode 100644 index 0000000000..9a6c57e7c1 --- /dev/null +++ b/android/KMAPro/facebookSDK/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.3" + + defaultConfig { + minSdkVersion 15 + targetSdkVersion 25 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + + lintOptions { + abortOnError false + disable 'ValidFragment' + } +} + +dependencies { + compile 'com.android.support:support-v4:25.4.0' + compile files('libs/bolts.jar') +} diff --git a/android/KMAPro/facebookSDK/libs/bolts.jar b/android/KMAPro/facebookSDK/libs/bolts.jar new file mode 100644 index 0000000000..9a9a1e94b7 Binary files /dev/null and b/android/KMAPro/facebookSDK/libs/bolts.jar differ diff --git a/android/KMAPro/facebookSDK/src/main/AndroidManifest.xml b/android/KMAPro/facebookSDK/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..517b945c8d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/AccessToken.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AccessToken.java new file mode 100644 index 0000000000..392d677e3e --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AccessToken.java @@ -0,0 +1,428 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import com.facebook.internal.NativeProtocol; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; + +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * This class represents an access token returned by the Facebook Login service, along with associated + * metadata such as its expiration date and permissions. In general, the {@link Session} class will + * abstract away the need to worry about the details of an access token, but there are situations + * (such as handling native links, importing previously-obtained access tokens, etc.) where it is + * useful to deal with access tokens directly. Factory methods are provided to construct access tokens. + *

+ * For more information on access tokens, see + * Access Tokens. + */ +public final class AccessToken implements Serializable { + private static final long serialVersionUID = 1L; + static final String ACCESS_TOKEN_KEY = "access_token"; + static final String EXPIRES_IN_KEY = "expires_in"; + private static final Date MIN_DATE = new Date(Long.MIN_VALUE); + private static final Date MAX_DATE = new Date(Long.MAX_VALUE); + private static final Date DEFAULT_EXPIRATION_TIME = MAX_DATE; + private static final Date DEFAULT_LAST_REFRESH_TIME = new Date(); + private static final AccessTokenSource DEFAULT_ACCESS_TOKEN_SOURCE = AccessTokenSource.FACEBOOK_APPLICATION_WEB; + private static final Date ALREADY_EXPIRED_EXPIRATION_TIME = MIN_DATE; + + private final Date expires; + private final List permissions; + private final List declinedPermissions; + private final String token; + private final AccessTokenSource source; + private final Date lastRefresh; + + AccessToken(String token, Date expires, List permissions, List declinedPermissions, AccessTokenSource source, Date lastRefresh) { + if (permissions == null) { + permissions = Collections.emptyList(); + } + if (declinedPermissions == null) { + declinedPermissions = Collections.emptyList(); + } + + this.expires = expires; + this.permissions = Collections.unmodifiableList(permissions); + this.declinedPermissions = Collections.unmodifiableList(declinedPermissions); + this.token = token; + this.source = source; + this.lastRefresh = lastRefresh; + } + + /** + * Gets the string representing the access token. + * + * @return the string representing the access token + */ + public String getToken() { + return this.token; + } + + /** + * Gets the date at which the access token expires. + * + * @return the expiration date of the token + */ + public Date getExpires() { + return this.expires; + } + + /** + * Gets the list of permissions associated with this access token. Note that the most up-to-date + * list of permissions is maintained by the Facebook service, so this list may be outdated if + * permissions have been added or removed since the time the AccessToken object was created. For + * more information on permissions, see https://developers.facebook.com/docs/reference/login/#permissions. + * + * @return a read-only list of strings representing the permissions granted via this access token + */ + public List getPermissions() { + return this.permissions; + } + + /** + * Gets the list of permissions declined by the user with this access token. It represents the entire set + * of permissions that have been requested and declined. Note that the most up-to-date list of permissions is + * maintained by the Facebook service, so this list may be outdated if permissions have been granted or declined + * since the last time an AccessToken object was created. + * + * @return a read-only list of strings representing the permissions declined by the user + */ + public List getDeclinedPermissions() { + return this.declinedPermissions; + } + + /** + * Gets the {@link AccessTokenSource} indicating how this access token was obtained. + * + * @return the enum indicating how the access token was obtained + */ + public AccessTokenSource getSource() { + return source; + } + + /** + * Gets the date at which the token was last refreshed. Since tokens expire, the Facebook SDK + * will attempt to renew them periodically. + * + * @return the date at which this token was last refreshed + */ + public Date getLastRefresh() { + return this.lastRefresh; + } + + /** + * Creates a new AccessToken using the supplied information from a previously-obtained access + * token (for instance, from an already-cached access token obtained prior to integration with the + * Facebook SDK). + * + * @param accessToken the access token string obtained from Facebook + * @param expirationTime the expiration date associated with the token; if null, an infinite expiration time is + * assumed (but will become correct when the token is refreshed) + * @param lastRefreshTime the last time the token was refreshed (or when it was first obtained); if null, + * the current time is used. + * @param accessTokenSource an enum indicating how the token was originally obtained (in most cases, + * this will be either AccessTokenSource.FACEBOOK_APPLICATION or + * AccessTokenSource.WEB_VIEW); if null, FACEBOOK_APPLICATION is assumed. + * @param permissions the permissions that were requested when the token was obtained (or when + * it was last reauthorized); may be null if permission set is unknown + * @return a new AccessToken + */ + public static AccessToken createFromExistingAccessToken(String accessToken, Date expirationTime, + Date lastRefreshTime, AccessTokenSource accessTokenSource, List permissions) { + if (expirationTime == null) { + expirationTime = DEFAULT_EXPIRATION_TIME; + } + if (lastRefreshTime == null) { + lastRefreshTime = DEFAULT_LAST_REFRESH_TIME; + } + if (accessTokenSource == null) { + accessTokenSource = DEFAULT_ACCESS_TOKEN_SOURCE; + } + + return new AccessToken(accessToken, expirationTime, permissions, null, accessTokenSource, lastRefreshTime); + } + + /** + * Creates a new AccessToken using the information contained in an Intent populated by the Facebook + * application in order to launch a native link. For more information on native linking, please see + * https://developers.facebook.com/docs/mobile/android/deep_linking/. + * + * @param intent the Intent that was used to start an Activity; must not be null + * @return a new AccessToken, or null if the Intent did not contain enough data to create one + */ + public static AccessToken createFromNativeLinkingIntent(Intent intent) { + Validate.notNull(intent, "intent"); + + if (intent.getExtras() == null) { + return null; + } + + return createFromBundle(null, intent.getExtras(), AccessTokenSource.FACEBOOK_APPLICATION_WEB, new Date()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("{AccessToken"); + builder.append(" token:").append(tokenToString()); + appendPermissions(builder); + builder.append("}"); + + return builder.toString(); + } + + static AccessToken createEmptyToken() { + return new AccessToken("", ALREADY_EXPIRED_EXPIRATION_TIME, null, null, AccessTokenSource.NONE, + DEFAULT_LAST_REFRESH_TIME); + } + + static AccessToken createFromString(String token, List permissions, AccessTokenSource source) { + return new AccessToken(token, DEFAULT_EXPIRATION_TIME, permissions, null, source, DEFAULT_LAST_REFRESH_TIME); + } + + static AccessToken createFromNativeLogin(Bundle bundle, AccessTokenSource source) { + Date expires = getBundleLongAsDate( + bundle, NativeProtocol.EXTRA_EXPIRES_SECONDS_SINCE_EPOCH, new Date(0)); + ArrayList permissions = bundle.getStringArrayList(NativeProtocol.EXTRA_PERMISSIONS); + String token = bundle.getString(NativeProtocol.EXTRA_ACCESS_TOKEN); + + return createNew(permissions, null, token, expires, source); + } + + static AccessToken createFromWebBundle(List requestedPermissions, Bundle bundle, AccessTokenSource source) { + Date expires = getBundleLongAsDate(bundle, EXPIRES_IN_KEY, new Date()); + String token = bundle.getString(ACCESS_TOKEN_KEY); + + // With Login v4, we now get back the actual permissions granted, so update the permissions to be the real thing + String grantedPermissions = bundle.getString("granted_scopes"); + if (!Utility.isNullOrEmpty(grantedPermissions)) { + requestedPermissions = new ArrayList(Arrays.asList(grantedPermissions.split(","))); + } + String deniedPermissions = bundle.getString("denied_scopes"); + List declinedPermissions = null; + if (!Utility.isNullOrEmpty(deniedPermissions)) { + declinedPermissions = new ArrayList(Arrays.asList(deniedPermissions.split(","))); + } + + return createNew(requestedPermissions, declinedPermissions, token, expires, source); + } + + @SuppressLint("FieldGetter") + static AccessToken createFromRefresh(AccessToken current, Bundle bundle) { + // Only tokens obtained via SSO support refresh. Token refresh returns the expiration date in + // seconds from the epoch rather than seconds from now. + assert (current.source == AccessTokenSource.FACEBOOK_APPLICATION_WEB || + current.source == AccessTokenSource.FACEBOOK_APPLICATION_NATIVE || + current.source == AccessTokenSource.FACEBOOK_APPLICATION_SERVICE); + + Date expires = getBundleLongAsDate(bundle, EXPIRES_IN_KEY, new Date(0)); + String token = bundle.getString(ACCESS_TOKEN_KEY); + + return createNew(current.getPermissions(), current.getDeclinedPermissions(), token, expires, current.source); + } + + static AccessToken createFromTokenWithRefreshedPermissions( + AccessToken token, + List grantedPermissions, + List declinedPermissions) { + return new AccessToken(token.token, token.expires, grantedPermissions, declinedPermissions, token.source, token.lastRefresh); + } + + private static AccessToken createNew( + List grantedPermissions, + List declinedPermissions, + String accessToken, Date expires, + AccessTokenSource source) { + if (Utility.isNullOrEmpty(accessToken) || (expires == null)) { + return createEmptyToken(); + } else { + return new AccessToken(accessToken, expires, grantedPermissions, declinedPermissions, source, new Date()); + } + } + + static AccessToken createFromCache(Bundle bundle) { + List permissions = getPermissionsFromBundle(bundle, TokenCachingStrategy.PERMISSIONS_KEY); + List declinedPermissions = getPermissionsFromBundle(bundle, TokenCachingStrategy.DECLINED_PERMISSIONS_KEY); + + return new AccessToken(bundle.getString(TokenCachingStrategy.TOKEN_KEY), TokenCachingStrategy.getDate(bundle, + TokenCachingStrategy.EXPIRATION_DATE_KEY), permissions, declinedPermissions, + TokenCachingStrategy.getSource(bundle), + TokenCachingStrategy.getDate(bundle, TokenCachingStrategy.LAST_REFRESH_DATE_KEY)); + } + + static List getPermissionsFromBundle(Bundle bundle, String key) { + // Copy the list so we can guarantee immutable + List originalPermissions = bundle.getStringArrayList(key); + List permissions; + if (originalPermissions == null) { + permissions = Collections.emptyList(); + } else { + permissions = Collections.unmodifiableList(new ArrayList(originalPermissions)); + } + return permissions; + } + + Bundle toCacheBundle() { + Bundle bundle = new Bundle(); + + bundle.putString(TokenCachingStrategy.TOKEN_KEY, this.token); + TokenCachingStrategy.putDate(bundle, TokenCachingStrategy.EXPIRATION_DATE_KEY, expires); + bundle.putStringArrayList(TokenCachingStrategy.PERMISSIONS_KEY, new ArrayList(permissions)); + bundle.putStringArrayList(TokenCachingStrategy.DECLINED_PERMISSIONS_KEY, new ArrayList(declinedPermissions)); + bundle.putSerializable(TokenCachingStrategy.TOKEN_SOURCE_KEY, source); + TokenCachingStrategy.putDate(bundle, TokenCachingStrategy.LAST_REFRESH_DATE_KEY, lastRefresh); + + return bundle; + } + + boolean isInvalid() { + return Utility.isNullOrEmpty(this.token) || new Date().after(this.expires); + } + + private static AccessToken createFromBundle(List requestedPermissions, Bundle bundle, + AccessTokenSource source, + Date expirationBase) { + String token = bundle.getString(ACCESS_TOKEN_KEY); + Date expires = getBundleLongAsDate(bundle, EXPIRES_IN_KEY, expirationBase); + + if (Utility.isNullOrEmpty(token) || (expires == null)) { + return null; + } + + return new AccessToken(token, expires, requestedPermissions, null, source, new Date()); + } + + private String tokenToString() { + if (this.token == null) { + return "null"; + } else if (Settings.isLoggingBehaviorEnabled(LoggingBehavior.INCLUDE_ACCESS_TOKENS)) { + return this.token; + } else { + return "ACCESS_TOKEN_REMOVED"; + } + } + + private void appendPermissions(StringBuilder builder) { + builder.append(" permissions:"); + if (this.permissions == null) { + builder.append("null"); + } else { + builder.append("["); + builder.append(TextUtils.join(", ", permissions)); + builder.append("]"); + } + } + + private static class SerializationProxyV1 implements Serializable { + private static final long serialVersionUID = -2488473066578201069L; + private final Date expires; + private final List permissions; + private final String token; + private final AccessTokenSource source; + private final Date lastRefresh; + + private SerializationProxyV1(String token, Date expires, + List permissions, AccessTokenSource source, Date lastRefresh) { + this.expires = expires; + this.permissions = permissions; + this.token = token; + this.source = source; + this.lastRefresh = lastRefresh; + } + + private Object readResolve() { + return new AccessToken(token, expires, permissions, null, source, lastRefresh); + } + } + + private static class SerializationProxyV2 implements Serializable { + private static final long serialVersionUID = -2488473066578201068L; + private final Date expires; + private final List permissions; + private final List declinedPermissions; + private final String token; + private final AccessTokenSource source; + private final Date lastRefresh; + + private SerializationProxyV2(String token, Date expires, + List permissions, List declinedPermissions, + AccessTokenSource source, Date lastRefresh) { + this.expires = expires; + this.permissions = permissions; + this.declinedPermissions = declinedPermissions; + this.token = token; + this.source = source; + this.lastRefresh = lastRefresh; + } + + private Object readResolve() { + return new AccessToken(token, expires, permissions, declinedPermissions, source, lastRefresh); + } + } + + private Object writeReplace() { + return new SerializationProxyV2(token, expires, permissions, declinedPermissions, source, lastRefresh); + } + + // have a readObject that throws to prevent spoofing + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Cannot readObject, serialization proxy required"); + } + + + private static Date getBundleLongAsDate(Bundle bundle, String key, Date dateBase) { + if (bundle == null) { + return null; + } + + long secondsFromBase = Long.MIN_VALUE; + + Object secondsObject = bundle.get(key); + if (secondsObject instanceof Long) { + secondsFromBase = (Long) secondsObject; + } else if (secondsObject instanceof String) { + try { + secondsFromBase = Long.parseLong((String) secondsObject); + } catch (NumberFormatException e) { + return null; + } + } else { + return null; + } + + if (secondsFromBase == 0) { + return new Date(Long.MAX_VALUE); + } else { + return new Date(dateBase.getTime() + (secondsFromBase * 1000L)); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/AccessTokenSource.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AccessTokenSource.java new file mode 100644 index 0000000000..2631112855 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AccessTokenSource.java @@ -0,0 +1,67 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Indicates where a Facebook access token was obtained from. + */ +public enum AccessTokenSource { + /** + * Indicates an access token has not been obtained, or is otherwise invalid. + */ + NONE(false), + /** + * Indicates an access token was obtained by the user logging in through the + * Facebook app for Android using the web login dialog. + */ + FACEBOOK_APPLICATION_WEB(true), + /** + * Indicates an access token was obtained by the user logging in through the + * Facebook app for Android using the native login dialog. + */ + FACEBOOK_APPLICATION_NATIVE(true), + /** + * Indicates an access token was obtained by asking the Facebook app for the + * current token based on permissions the user has already granted to the app. + * No dialog was shown to the user in this case. + */ + FACEBOOK_APPLICATION_SERVICE(true), + /** + * Indicates an access token was obtained by the user logging in through the + * Web-based dialog. + */ + WEB_VIEW(false), + /** + * Indicates an access token is for a test user rather than an actual + * Facebook user. + */ + TEST_USER(true), + /** + * Indicates an access token constructed with a Client Token. + */ + CLIENT_TOKEN(true); + + private final boolean canExtendToken; + + AccessTokenSource(boolean canExtendToken) { + this.canExtendToken = canExtendToken; + } + + boolean canExtendToken() { + return canExtendToken; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppEventsConstants.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppEventsConstants.java new file mode 100644 index 0000000000..24fd138721 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppEventsConstants.java @@ -0,0 +1,168 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Predefined event and parameter names for logging events common to many apps. Logging occurs through the + * {@link AppEventsLogger#logEvent(String, android.os.Bundle)} family of methods. + */ +public class AppEventsConstants { + // Event names + + // General purpose + + /** Log this event when an app is being activated. */ + public static final String EVENT_NAME_ACTIVATED_APP = "fb_mobile_activate_app"; + + /** Log this event when a user has completed registration with the app. */ + public static final String EVENT_NAME_COMPLETED_REGISTRATION = "fb_mobile_complete_registration"; + + /** Log this event when a user has viewed a form of content in the app. */ + public static final String EVENT_NAME_VIEWED_CONTENT = "fb_mobile_content_view"; + + /** Log this event when a user has performed a search within the app. */ + public static final String EVENT_NAME_SEARCHED = "fb_mobile_search"; + + /** + * Log this event when the user has rated an item in the app. + * The valueToSum passed to logEvent should be the numeric rating. + */ + public static final String EVENT_NAME_RATED = "fb_mobile_rate"; + + /** Log this event when the user has completed a tutorial in the app. */ + public static final String EVENT_NAME_COMPLETED_TUTORIAL = "fb_mobile_tutorial_completion"; + + // Ecommerce related + + /** + * Log this event when the user has added an item to their cart. + * The valueToSum passed to logEvent should be the item's price. + */ + public static final String EVENT_NAME_ADDED_TO_CART = "fb_mobile_add_to_cart"; + + /** + * Log this event when the user has added an item to their wishlist. + * The valueToSum passed to logEvent should be the item's price. + */ + public static final String EVENT_NAME_ADDED_TO_WISHLIST = "fb_mobile_add_to_wishlist"; + + /** + * Log this event when the user has entered the checkout process. + * The valueToSum passed to logEvent should be the total price in the cart. + */ + public static final String EVENT_NAME_INITIATED_CHECKOUT = "fb_mobile_initiated_checkout"; + + /** Log this event when the user has entered their payment info. */ + public static final String EVENT_NAME_ADDED_PAYMENT_INFO = "fb_mobile_add_payment_info"; + + /** + * Log this event when the user has completed a purchase. + * The {@link AppEventsLogger#logPurchase(java.math.BigDecimal, java.util.Currency)} method is a shortcut for + * logging this event. + */ + public static final String EVENT_NAME_PURCHASED = "fb_mobile_purchase"; + + // Gaming related + + /** Log this event when the user has achieved a level in the app. */ + public static final String EVENT_NAME_ACHIEVED_LEVEL = "fb_mobile_level_achieved"; + + /** Log this event when the user has unlocked an achievement in the app. */ + public static final String EVENT_NAME_UNLOCKED_ACHIEVEMENT = "fb_mobile_achievement_unlocked"; + + /** + * Log this event when the user has spent app credits. + * The valueToSum passed to logEvent should be the number of credits spent. + */ + public static final String EVENT_NAME_SPENT_CREDITS = "fb_mobile_spent_credits"; + + + + + // Event parameters + + /** + * Parameter key used to specify currency used with logged event. E.g. "USD", "EUR", "GBP". + * See ISO-4217 for specific values. One reference for these is . + */ + public static final String EVENT_PARAM_CURRENCY = "fb_currency"; + + /** + * Parameter key used to specify method user has used to register for the app, e.g., "Facebook", "email", + * "Twitter", etc. + */ + public static final String EVENT_PARAM_REGISTRATION_METHOD = "fb_registration_method"; + + /** + * Parameter key used to specify a generic content type/family for the logged event, e.g. "music", "photo", + * "video". Options to use will vary based upon what the app is all about. + */ + public static final String EVENT_PARAM_CONTENT_TYPE = "fb_content_type"; + + /** + * Parameter key used to specify an ID for the specific piece of content being logged about. + * Could be an EAN, article identifier, etc., depending on the nature of the app. + */ + public static final String EVENT_PARAM_CONTENT_ID = "fb_content_id"; + + /** Parameter key used to specify the string provided by the user for a search operation. */ + public static final String EVENT_PARAM_SEARCH_STRING = "fb_search_string"; + + /** + * Parameter key used to specify whether the activity being logged about was successful or not. + * EVENT_PARAM_VALUE_YES and EVENT_PARAM_VALUE_NO are good canonical values to use for this parameter. + */ + public static final String EVENT_PARAM_SUCCESS = "fb_success"; + + /** + * Parameter key used to specify the maximum rating available for the EVENT_NAME_RATE event. + * E.g., "5" or "10". + */ + public static final String EVENT_PARAM_MAX_RATING_VALUE = "fb_max_rating_value"; + + /** + * Parameter key used to specify whether payment info is available for the EVENT_NAME_INITIATED_CHECKOUT event. + * EVENT_PARAM_VALUE_YES and EVENT_PARAM_VALUE_NO are good canonical values to use for this parameter. + */ + public static final String EVENT_PARAM_PAYMENT_INFO_AVAILABLE = "fb_payment_info_available"; + + /** + * Parameter key used to specify how many items are being processed for an EVENT_NAME_INITIATED_CHECKOUT + * or EVENT_NAME_PURCHASE event. + */ + public static final String EVENT_PARAM_NUM_ITEMS = "fb_num_items"; + + /** Parameter key used to specify the level achieved in a EVENT_NAME_LEVEL_ACHIEVED event. */ + public static final String EVENT_PARAM_LEVEL = "fb_level"; + + /** + * Parameter key used to specify a description appropriate to the event being logged. + * E.g., the name of the achievement unlocked in the EVENT_NAME_ACHIEVEMENT_UNLOCKED event. + */ + public static final String EVENT_PARAM_DESCRIPTION = "fb_description"; + + + + // Parameter values + + /** Yes-valued parameter value to be used with parameter keys that need a Yes/No value */ + public static final String EVENT_PARAM_VALUE_YES = "1"; + + /** No-valued parameter value to be used with parameter keys that need a Yes/No value */ + public static final String EVENT_PARAM_VALUE_NO = "0"; + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppEventsLogger.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppEventsLogger.java new file mode 100644 index 0000000000..20e4c24a8f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppEventsLogger.java @@ -0,0 +1,1303 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import com.facebook.internal.*; +import com.facebook.model.GraphObject; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.*; +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + + +/** + *

+ * The AppEventsLogger class allows the developer to log various types of events back to Facebook. In order to log + * events, the app must create an instance of this class via a {@link #newLogger newLogger} method, and then call + * the various "log" methods off of that. + *

+ *

+ * This client-side event logging is then available through Facebook App Insights + * and for use with Facebook Ads conversion tracking and optimization. + *

+ *

+ * The AppEventsLogger class has a few related roles: + *

    + *
  • + * Logging predefined and application-defined events to Facebook App Insights with a + * numeric value to sum across a large number of events, and an optional set of key/value + * parameters that define "segments" for this event (e.g., 'purchaserStatus' : 'frequent', or + * 'gamerLevel' : 'intermediate'). These events may also be used for ads conversion tracking, + * optimization, and other ads related targeting in the future. + *
  • + *
  • + * Methods that control the way in which events are flushed out to the Facebook servers. + *
  • + *
+ * Here are some important characteristics of the logging mechanism provided by AppEventsLogger: + *
    + *
  • + * Events are not sent immediately when logged. They're cached and flushed out to the Facebook servers + * in a number of situations: + *
      + *
    • when an event count threshold is passed (currently 100 logged events).
    • + *
    • when a time threshold is passed (currently 60 seconds).
    • + *
    • when an app has gone to background and is then brought back to the foreground.
    • + *
    + *
  • + * Events will be accumulated when the app is in a disconnected state, and sent when the connection is + * restored and one of the above 'flush' conditions are met. + *
  • + *
  • + * The AppEventsLogger class is intended to be used from the thread it was created on. Multiple AppEventsLoggers + * may be created on other threads if desired. + *
  • + *
  • + * The developer can call the setFlushBehavior method to force the flushing of events to only + * occur on an explicit call to the `flush` method. + *
  • + *
  • + * The developer can turn on console debug output for event logging and flushing to the server + * Settings.addLoggingBehavior(LoggingBehavior.APP_EVENTS); + *
  • + *
+ * Some things to note when logging events: + *
    + *
  • + * There is a limit on the number of unique event names an app can use, on the order of 300. + *
  • + *
  • + * There is a limit to the number of unique parameter names in the provided parameters that can + * be used per event, on the order of 25. This is not just for an individual call, but for all + * invocations for that eventName. + *
  • + *
  • + * Event names and parameter names (the keys in the NSDictionary) must be between 2 and 40 characters, and + * must consist of alphanumeric characters, _, -, or spaces. + *
  • + *
  • + * The length of each parameter value can be no more than on the order of 100 characters. + *
  • + *
+ */ +public class AppEventsLogger { + // Enums + + /** + * Controls when an AppEventsLogger sends log events to the server + */ + public enum FlushBehavior { + /** + * Flush automatically: periodically (once a minute or after every 100 events), and always at app reactivation. + * This is the default value. + */ + AUTO, + + /** + * Only flush when AppEventsLogger.flush() is explicitly invoked. + */ + EXPLICIT_ONLY, + } + + private enum SuppressionTimeoutBehavior { + // Successfully logging an event will reset the timeout period (i.e., events will log no more than every N + // seconds). + RESET_TIMEOUT_WHEN_LOG_SUCCESSFUL, + // Attempting to log an event, even if it is suppressed, will reset the timeout period (i.e., events will not + // be logged until they have been "silent" for at least N seconds). + RESET_TIMEOUT_WHEN_LOG_ATTEMPTED, + } + + private static class EventSuppression { + // Timeout period in seconds + private int timeoutPeriod; + private SuppressionTimeoutBehavior behavior; + + EventSuppression(int timeoutPeriod, SuppressionTimeoutBehavior behavior) { + this.timeoutPeriod = timeoutPeriod; + this.behavior = behavior; + } + + int getTimeoutPeriod() { + return timeoutPeriod; + } + + SuppressionTimeoutBehavior getBehavior() { + return behavior; + } + } + + // Constants + private static final String TAG = AppEventsLogger.class.getCanonicalName(); + + private static final int NUM_LOG_EVENTS_TO_TRY_TO_FLUSH_AFTER = 100; + private static final int FLUSH_PERIOD_IN_SECONDS = 60; + private static final int APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS = 60 * 60 * 24; + private static final int APP_ACTIVATE_SUPPRESSION_PERIOD_IN_SECONDS = 5 * 60; + + // Instance member variables + private final Context context; + private final AccessTokenAppIdPair accessTokenAppId; + + private static Map stateMap = + new ConcurrentHashMap(); + private static Timer flushTimer; + private static Timer supportsAttributionRecheckTimer; + private static FlushBehavior flushBehavior = FlushBehavior.AUTO; + private static boolean requestInFlight; + private static Context applicationContext; + private static Object staticLock = new Object(); + private static String hashedDeviceAndAppId; + private static Map mapEventsToSuppressionTime = new HashMap(); + @SuppressWarnings("serial") + private static Map mapEventNameToSuppress = new HashMap() { + { + put(AppEventsConstants.EVENT_NAME_ACTIVATED_APP, + new EventSuppression(APP_ACTIVATE_SUPPRESSION_PERIOD_IN_SECONDS, + SuppressionTimeoutBehavior.RESET_TIMEOUT_WHEN_LOG_ATTEMPTED)); + } + }; + + // Rather than retaining Sessions, we extract the information we need and track app events by + // application ID and access token (which may be null for Session-less calls). This avoids needing to + // worry about Session lifecycle and also allows us to coalesce app events from different Sessions + // that have the same access token/app ID. + private static class AccessTokenAppIdPair implements Serializable { + private static final long serialVersionUID = 1L; + private final String accessToken; + private final String applicationId; + + AccessTokenAppIdPair(Session session) { + this(session.getAccessToken(), session.getApplicationId()); + } + + AccessTokenAppIdPair(String accessToken, String applicationId) { + this.accessToken = Utility.isNullOrEmpty(accessToken) ? null : accessToken; + this.applicationId = applicationId; + } + + String getAccessToken() { + return accessToken; + } + + String getApplicationId() { + return applicationId; + } + + @Override + public int hashCode() { + return (accessToken == null ? 0 : accessToken.hashCode()) ^ + (applicationId == null ? 0 : applicationId.hashCode()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AccessTokenAppIdPair)) { + return false; + } + AccessTokenAppIdPair p = (AccessTokenAppIdPair) o; + return Utility.areObjectsEqual(p.accessToken, accessToken) && + Utility.areObjectsEqual(p.applicationId, applicationId); + } + + private static class SerializationProxyV1 implements Serializable { + private static final long serialVersionUID = -2488473066578201069L; + private final String accessToken; + private final String appId; + + private SerializationProxyV1(String accessToken, String appId) { + this.accessToken = accessToken; + this.appId = appId; + } + + private Object readResolve() { + return new AccessTokenAppIdPair(accessToken, appId); + } + } + + private Object writeReplace() { + return new SerializationProxyV1(accessToken, applicationId); + } + } + + /** + * This method is deprecated. Use {@link Settings#getLimitEventAndDataUsage(Context)} instead. + */ + @Deprecated + public static boolean getLimitEventUsage(Context context) { + return Settings.getLimitEventAndDataUsage(context); + } + + /** + * This method is deprecated. Use {@link Settings#setLimitEventAndDataUsage(Context, boolean)} instead. + */ + @Deprecated + public static void setLimitEventUsage(Context context, boolean limitEventUsage) { + Settings.setLimitEventAndDataUsage(context, limitEventUsage); + } + + /** + * Notifies the events system that the app has launched & logs an activatedApp event. Should be called whenever + * your app becomes active, typically in the onResume() method of each long-running Activity of your app. + * + * Use this method if your application ID is stored in application metadata, otherwise see + * {@link AppEventsLogger#activateApp(android.content.Context, String)}. + * + * @param context Used to access the applicationId and the attributionId for non-authenticated users. + */ + public static void activateApp(Context context) { + activateApp(context, Utility.getMetadataApplicationId(context)); + } + + /** + * Notifies the events system that the app has launched & logs an activatedApp event. Should be called whenever + * your app becomes active, typically in the onResume() method of each long-running Activity of your app. + * + * @param context Used to access the attributionId for non-authenticated users. + * + * @param applicationId The specific applicationId to report the activation for. + */ + @SuppressWarnings("deprecation") + public static void activateApp(Context context, String applicationId) { + if (context == null || applicationId == null) { + throw new IllegalArgumentException("Both context and applicationId must be non-null"); + } + + // activateApp supercedes publishInstall in the public API, so we need to explicitly invoke it, since the server + // can't reliably infer install state for all conditions of an app activate. + Settings.publishInstallAsync(context, applicationId); + + AppEventsLogger logger = new AppEventsLogger(context, applicationId, null); + logger.logEvent(AppEventsConstants.EVENT_NAME_ACTIVATED_APP); + } + + /** + * Build an AppEventsLogger instance to log events through. The Facebook app that these events are targeted at + * comes from this application's metadata. The application ID used to log events will be determined from + * the app ID specified in the package metadata. + * + * @param context Used to access the applicationId and the attributionId for non-authenticated users. + * + * @return AppEventsLogger instance to invoke log* methods on. + */ + public static AppEventsLogger newLogger(Context context) { + return new AppEventsLogger(context, null, null); + } + + /** + * Build an AppEventsLogger instance to log events through. + * + * @param context Used to access the attributionId for non-authenticated users. + * @param session Explicitly specified Session to log events against. If null, the activeSession + * will be used if it's open, otherwise the logging will happen against the default + * app ID specified via the app ID specified in the package metadata. + * + * @return AppEventsLogger instance to invoke log* methods on. + */ + public static AppEventsLogger newLogger(Context context, Session session) { + return new AppEventsLogger(context, null, session); + } + + /** + * Build an AppEventsLogger instance to log events through. + * + * @param context Used to access the attributionId for non-authenticated users. + * @param applicationId Explicitly specified Facebook applicationId to log events against. If null, the default + * app ID specified in the package metadata will be used. + * @param session Explicitly specified Session to log events against. If null, the activeSession + * will be used if it's open, otherwise the logging will happen against the specified + * app ID. + * + * @return AppEventsLogger instance to invoke log* methods on. + */ + public static AppEventsLogger newLogger(Context context, String applicationId, Session session) { + return new AppEventsLogger(context, applicationId, session); + } + + /** + * Build an AppEventsLogger instance to log events that are attributed to the application but not to + * any particular Session. + * + * @param context Used to access the attributionId for non-authenticated users. + * @param applicationId Explicitly specified Facebook applicationId to log events against. If null, the default + * app ID specified + * in the package metadata will be used. + * + * @return AppEventsLogger instance to invoke log* methods on. + */ + public static AppEventsLogger newLogger(Context context, String applicationId) { + return new AppEventsLogger(context, applicationId, null); + } + + /** + * The action used to indicate that a flush of app events has occurred. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_APP_EVENTS_FLUSHED = "com.facebook.sdk.APP_EVENTS_FLUSHED"; + + public static final String APP_EVENTS_EXTRA_NUM_EVENTS_FLUSHED = "com.facebook.sdk.APP_EVENTS_NUM_EVENTS_FLUSHED"; + public static final String APP_EVENTS_EXTRA_FLUSH_RESULT = "com.facebook.sdk.APP_EVENTS_FLUSH_RESULT"; + + /** + * Access the behavior that AppEventsLogger uses to determine when to flush logged events to the server. This + * setting applies to all instances of AppEventsLogger. + * @return specified flush behavior. + */ + public static FlushBehavior getFlushBehavior() { + synchronized (staticLock) { + return flushBehavior; + } + } + + /** + * Set the behavior that this AppEventsLogger uses to determine when to flush logged events to the server. This + * setting applies to all instances of AppEventsLogger. + * @param flushBehavior the desired behavior. + */ + public static void setFlushBehavior(FlushBehavior flushBehavior) { + synchronized (staticLock) { + AppEventsLogger.flushBehavior = flushBehavior; + } + } + + /** + * Log an app event with the specified name. + * @param eventName eventName used to denote the event. Choose amongst the EVENT_NAME_* constants in + * {@link AppEventsConstants} when possible. Or create your own if none of the EVENT_NAME_* + * constants are applicable. + * Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores + * or hyphens, but mustn't have a space or hyphen as the first character. Any given app should + * have no more than ~300 distinct event names. + */ + public void logEvent(String eventName) { + logEvent(eventName, null); + } + + /** + * Log an app event with the specified name and the supplied value. + * @param eventName eventName used to denote the event. Choose amongst the EVENT_NAME_* constants in + * {@link AppEventsConstants} when possible. Or create your own if none of the EVENT_NAME_* + * constants are applicable. + * Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores + * or hyphens, but mustn't have a space or hyphen as the first character. Any given app should + * have no more than ~300 distinct event names. + * * @param eventName + * @param valueToSum a value to associate with the event which will be summed up in Insights for across all + * instances of the event, so that average values can be determined, etc. + */ + public void logEvent(String eventName, double valueToSum) { + logEvent(eventName, valueToSum, null); + } + + /** + * Log an app event with the specified name and set of parameters. + * @param eventName eventName used to denote the event. Choose amongst the EVENT_NAME_* constants in + * {@link AppEventsConstants} when possible. Or create your own if none of the EVENT_NAME_* + * constants are applicable. + * Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores + * or hyphens, but mustn't have a space or hyphen as the first character. Any given app should + * have no more than ~300 distinct event names. + * @param parameters A Bundle of parameters to log with the event. Insights will allow looking at the logs of these + * events via different parameter values. You can log on the order of 10 parameters with each + * distinct eventName. It's advisable to keep the number of unique values provided for each + * parameter in the, at most, thousands. As an example, don't attempt to provide a unique + * parameter value for each unique user in your app. You won't get meaningful aggregate reporting + * on so many parameter values. The values in the bundles should be Strings or numeric values. + */ + public void logEvent(String eventName, Bundle parameters) { + logEvent(eventName, null, parameters, false); + } + + /** + * Log an app event with the specified name, supplied value, and set of parameters. + * @param eventName eventName used to denote the event. Choose amongst the EVENT_NAME_* constants in + * {@link AppEventsConstants} when possible. Or create your own if none of the EVENT_NAME_* + * constants are applicable. + * Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores + * or hyphens, but mustn't have a space or hyphen as the first character. Any given app should + * have no more than ~300 distinct event names. + * @param valueToSum a value to associate with the event which will be summed up in Insights for across all + * instances of the event, so that average values can be determined, etc. + * @param parameters A Bundle of parameters to log with the event. Insights will allow looking at the logs of these + * events via different parameter values. You can log on the order of 10 parameters with each + * distinct eventName. It's advisable to keep the number of unique values provided for each + * parameter in the, at most, thousands. As an example, don't attempt to provide a unique + * parameter value for each unique user in your app. You won't get meaningful aggregate reporting + * on so many parameter values. The values in the bundles should be Strings or numeric values. + */ + public void logEvent(String eventName, double valueToSum, Bundle parameters) { + logEvent(eventName, valueToSum, parameters, false); + } + + /** + * Logs a purchase event with Facebook, in the specified amount and with the specified currency. + * + * @param purchaseAmount Amount of purchase, in the currency specified by the 'currency' parameter. This value + * will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346). + * @param currency Currency used to specify the amount. + */ + public void logPurchase(BigDecimal purchaseAmount, Currency currency) { + logPurchase(purchaseAmount, currency, null); + } + + /** + * Logs a purchase event with Facebook, in the specified amount and with the specified currency. Additional + * detail about the purchase can be passed in through the parameters bundle. + * + * @param purchaseAmount Amount of purchase, in the currency specified by the 'currency' parameter. This value + * will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346). + * @param currency Currency used to specify the amount. + * @param parameters Arbitrary additional information for describing this event. Should have no more than + * 10 entries, and keys should be mostly consistent from one purchase event to the next. + */ + public void logPurchase(BigDecimal purchaseAmount, Currency currency, Bundle parameters) { + + if (purchaseAmount == null) { + notifyDeveloperError("purchaseAmount cannot be null"); + return; + } else if (currency == null) { + notifyDeveloperError("currency cannot be null"); + return; + } + + if (parameters == null) { + parameters = new Bundle(); + } + parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.getCurrencyCode()); + + logEvent(AppEventsConstants.EVENT_NAME_PURCHASED, purchaseAmount.doubleValue(), parameters); + eagerFlush(); + } + + /** + * Explicitly flush any stored events to the server. Implicit flushes may happen depending on the value + * of getFlushBehavior. This method allows for explicit, app invoked flushing. + */ + public void flush() { + flush(FlushReason.EXPLICIT); + } + + /** + * Call this when the consuming Activity/Fragment receives an onStop() callback in order to persist any + * outstanding events to disk, so they may be flushed at a later time. The next flush (explicit or not) + * will check for any outstanding events and, if present, include them in that flush. Note that this call + * may trigger an I/O operation on the calling thread. Explicit use of this method is not necessary + * if the consumer is making use of {@link UiLifecycleHelper}, which will take care of making the call + * in its own onStop() callback. + */ + public static void onContextStop() { + PersistedEvents.persistEvents(applicationContext, stateMap); + } + + boolean isValidForSession(Session session) { + AccessTokenAppIdPair other = new AccessTokenAppIdPair(session); + return accessTokenAppId.equals(other); + } + + /** + * This method is intended only for internal use by the Facebook SDK and other use is unsupported. + */ + public void logSdkEvent(String eventName, Double valueToSum, Bundle parameters) { + logEvent(eventName, valueToSum, parameters, true); + } + + /** + * Returns the app ID this logger was configured to log to. + * @return the Facebook app ID + */ + public String getApplicationId() { + return accessTokenAppId.getApplicationId(); + } + + // + // Private implementation + // + + private enum FlushReason { + EXPLICIT, + TIMER, + SESSION_CHANGE, + PERSISTED_EVENTS, + EVENT_THRESHOLD, + EAGER_FLUSHING_EVENT, + } + + private enum FlushResult { + SUCCESS, + SERVER_ERROR, + NO_CONNECTIVITY, + UNKNOWN_ERROR + } + + /** + * Constructor is private, newLogger() methods should be used to build an instance. + */ + private AppEventsLogger(Context context, String applicationId, Session session) { + + Validate.notNull(context, "context"); + this.context = context; + + if (session == null) { + session = Session.getActiveSession(); + } + + if (session != null) { + accessTokenAppId = new AccessTokenAppIdPair(session); + } else { + if (applicationId == null) { + applicationId = Utility.getMetadataApplicationId(context); + } + accessTokenAppId = new AccessTokenAppIdPair(null, applicationId); + } + + synchronized (staticLock) { + + if (hashedDeviceAndAppId == null) { + hashedDeviceAndAppId = Utility.getHashedDeviceAndAppID(context, applicationId); + } + + if (applicationContext == null) { + applicationContext = context.getApplicationContext(); + } + } + + initializeTimersIfNeeded(); + } + + private static void initializeTimersIfNeeded() { + synchronized (staticLock) { + if (flushTimer != null) { + return; + } + flushTimer = new Timer(); + supportsAttributionRecheckTimer = new Timer(); + } + + flushTimer.schedule( + new TimerTask() { + @Override + public void run() { + if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) { + flushAndWait(FlushReason.TIMER); + } + } + }, + 0, // start immediately + FLUSH_PERIOD_IN_SECONDS * 1000); + + supportsAttributionRecheckTimer.schedule( + new TimerTask() { + @Override + public void run() { + Set applicationIds = new HashSet(); + synchronized (staticLock) { + for (AccessTokenAppIdPair accessTokenAppId : stateMap.keySet()) { + applicationIds.add(accessTokenAppId.getApplicationId()); + } + } + for (String applicationId : applicationIds) { + Utility.queryAppSettings(applicationId, true); + } + } + }, + 0, // start immediately + APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS * 1000); + } + + private void logEvent(String eventName, Double valueToSum, Bundle parameters, boolean isImplicitlyLogged) { + + AppEvent event = new AppEvent(eventName, valueToSum, parameters, isImplicitlyLogged); + logEvent(context, event, accessTokenAppId); + } + + private static void logEvent(Context context, AppEvent event, AccessTokenAppIdPair accessTokenAppId) { + if(shouldSuppressEvent(event)) { + return; + } + + SessionEventsState state = getSessionEventsState(context, accessTokenAppId); + state.addEvent(event); + + flushIfNecessary(); + } + + // This will also update the timestamp based on specified behavior. + private static boolean shouldSuppressEvent(AppEvent event) { + EventSuppression suppressionInfo = mapEventNameToSuppress.get(event.getName()); + if (suppressionInfo == null) { + return false; + } + + Date timestamp = mapEventsToSuppressionTime.get(event.getName()); + boolean suppressed; + if (timestamp == null) { + suppressed = false; + } else { + long delta = new Date().getTime() - timestamp.getTime(); + suppressed = delta < (suppressionInfo.getTimeoutPeriod() * 1000); + } + + // Update the time if we're not suppressed, OR if we are suppressed but the behavior is to reset even on + // suppressed events. + if (!suppressed || + suppressionInfo.getBehavior() == SuppressionTimeoutBehavior.RESET_TIMEOUT_WHEN_LOG_ATTEMPTED) { + mapEventsToSuppressionTime.put(event.getName(), new Date()); + } + + return suppressed; + } + + static void eagerFlush() { + if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) { + flush(FlushReason.EAGER_FLUSHING_EVENT); + } + } + + private static void flushIfNecessary() { + synchronized (staticLock) { + if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) { + if (getAccumulatedEventCount() > NUM_LOG_EVENTS_TO_TRY_TO_FLUSH_AFTER) { + flush(FlushReason.EVENT_THRESHOLD); + } + } + } + } + + private static int getAccumulatedEventCount() { + synchronized (staticLock) { + + int result = 0; + for (SessionEventsState state : stateMap.values()) { + result += state.getAccumulatedEventCount(); + } + return result; + } + } + + // Creates a new SessionEventsState if not already in the map. + private static SessionEventsState getSessionEventsState(Context context, AccessTokenAppIdPair accessTokenAppId) { + synchronized (staticLock) { + SessionEventsState state = stateMap.get(accessTokenAppId); + if (state == null) { + // Retrieve attributionId, but we will only send it if attribution is supported for the app. + AttributionIdentifiers attributionIdentifiers = + AttributionIdentifiers.getAttributionIdentifiers(context); + + state = new SessionEventsState(attributionIdentifiers, context.getPackageName(), hashedDeviceAndAppId); + stateMap.put(accessTokenAppId, state); + } + return state; + } + } + + private static SessionEventsState getSessionEventsState(AccessTokenAppIdPair accessTokenAppId) { + synchronized (staticLock) { + return stateMap.get(accessTokenAppId); + } + } + + private static void flush(final FlushReason reason) { + + Settings.getExecutor().execute(new Runnable() { + @Override + public void run() { + flushAndWait(reason); + } + }); + } + + private static void flushAndWait(final FlushReason reason) { + + Set keysToFlush; + synchronized (staticLock) { + if (requestInFlight) { + return; + } + requestInFlight = true; + keysToFlush = new HashSet(stateMap.keySet()); + } + + accumulatePersistedEvents(); + + FlushStatistics flushResults = null; + try { + flushResults = buildAndExecuteRequests(reason, keysToFlush); + } catch (Exception e) { + Log.d(TAG, "Caught unexpected exception while flushing: " + e.toString()); + } + + synchronized (staticLock) { + requestInFlight = false; + } + + if (flushResults != null) { + final Intent intent = new Intent(ACTION_APP_EVENTS_FLUSHED); + intent.putExtra(APP_EVENTS_EXTRA_NUM_EVENTS_FLUSHED, flushResults.numEvents); + intent.putExtra(APP_EVENTS_EXTRA_FLUSH_RESULT, flushResults.result); + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent); + } + } + + private static FlushStatistics buildAndExecuteRequests(FlushReason reason, Set keysToFlush) { + FlushStatistics flushResults = new FlushStatistics(); + + boolean limitEventUsage = Settings.getLimitEventAndDataUsage(applicationContext); + + List requestsToExecute = new ArrayList(); + for (AccessTokenAppIdPair accessTokenAppId : keysToFlush) { + SessionEventsState sessionEventsState = getSessionEventsState(accessTokenAppId); + if (sessionEventsState == null) { + continue; + } + + Request request = buildRequestForSession(accessTokenAppId, sessionEventsState, limitEventUsage, + flushResults); + if (request != null) { + requestsToExecute.add(request); + } + } + + if (requestsToExecute.size() > 0) { + Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Flushing %d events due to %s.", + flushResults.numEvents, + reason.toString()); + + for (Request request : requestsToExecute) { + // Execute the request synchronously. Callbacks will take care of handling errors and updating + // our final overall result. + request.executeAndWait(); + } + return flushResults; + } + + return null; + } + + private static class FlushStatistics { + public int numEvents = 0; + public FlushResult result = FlushResult.SUCCESS; + } + + private static Request buildRequestForSession(final AccessTokenAppIdPair accessTokenAppId, + final SessionEventsState sessionEventsState, final boolean limitEventUsage, + final FlushStatistics flushState) { + String applicationId = accessTokenAppId.getApplicationId(); + + Utility.FetchedAppSettings fetchedAppSettings = Utility.queryAppSettings(applicationId, false); + + final Request postRequest = Request.newPostRequest( + null, + String.format("%s/activities", applicationId), + null, + null); + + Bundle requestParameters = postRequest.getParameters(); + if (requestParameters == null) { + requestParameters = new Bundle(); + } + requestParameters.putString("access_token", accessTokenAppId.getAccessToken()); + postRequest.setParameters(requestParameters); + + int numEvents = sessionEventsState.populateRequest(postRequest, fetchedAppSettings.supportsImplicitLogging(), + fetchedAppSettings.supportsAttribution(), limitEventUsage); + if (numEvents == 0) { + return null; + } + + flushState.numEvents += numEvents; + + postRequest.setCallback(new Request.Callback() { + @Override + public void onCompleted(Response response) { + handleResponse(accessTokenAppId, postRequest, response, sessionEventsState, flushState); + } + }); + + return postRequest; + } + + private static void handleResponse(AccessTokenAppIdPair accessTokenAppId, Request request, Response response, + SessionEventsState sessionEventsState, FlushStatistics flushState) { + FacebookRequestError error = response.getError(); + String resultDescription = "Success"; + + FlushResult flushResult = FlushResult.SUCCESS; + + if (error != null) { + final int NO_CONNECTIVITY_ERROR_CODE = -1; + if (error.getErrorCode() == NO_CONNECTIVITY_ERROR_CODE) { + resultDescription = "Failed: No Connectivity"; + flushResult = FlushResult.NO_CONNECTIVITY; + } else { + resultDescription = String.format("Failed:\n Response: %s\n Error %s", + response.toString(), + error.toString()); + flushResult = FlushResult.SERVER_ERROR; + } + } + + if (Settings.isLoggingBehaviorEnabled(LoggingBehavior.APP_EVENTS)) { + String eventsJsonString = (String) request.getTag(); + String prettyPrintedEvents; + + try { + JSONArray jsonArray = new JSONArray(eventsJsonString); + prettyPrintedEvents = jsonArray.toString(2); + } catch (JSONException exc) { + prettyPrintedEvents = ""; + } + + Logger.log(LoggingBehavior.APP_EVENTS, TAG, + "Flush completed\nParams: %s\n Result: %s\n Events JSON: %s", + request.getGraphObject().toString(), + resultDescription, + prettyPrintedEvents); + } + + sessionEventsState.clearInFlightAndStats(error != null); + + if (flushResult == FlushResult.NO_CONNECTIVITY) { + // We may call this for multiple requests in a batch, which is slightly inefficient since in principle + // we could call it once for all failed requests, but the impact is likely to be minimal. + // We don't call this for other server errors, because if an event failed because it was malformed, etc., + // continually retrying it will cause subsequent events to not be logged either. + PersistedEvents.persistEvents(applicationContext, accessTokenAppId, sessionEventsState); + } + + if (flushResult != FlushResult.SUCCESS) { + // We assume that connectivity issues are more significant to report than server issues. + if (flushState.result != FlushResult.NO_CONNECTIVITY) { + flushState.result = flushResult; + } + } + } + + private static int accumulatePersistedEvents() { + PersistedEvents persistedEvents = PersistedEvents.readAndClearStore(applicationContext); + + int result = 0; + for(AccessTokenAppIdPair accessTokenAppId : persistedEvents.keySet()) { + SessionEventsState sessionEventsState = getSessionEventsState(applicationContext, accessTokenAppId); + + List events = persistedEvents.getEvents(accessTokenAppId); + sessionEventsState.accumulatePersistedEvents(events); + result += events.size(); + } + + return result; + } + + /** + * Invoke this method, rather than throwing an Exception, for situations where user/server input might reasonably + * cause this to occur, and thus don't want an exception thrown at production time, but do want logging + * notification. + */ + private static void notifyDeveloperError(String message) { + Logger.log(LoggingBehavior.DEVELOPER_ERRORS, "AppEvents", message); + } + + + // + // Deprecated Stuff + // + + + static class SessionEventsState { + private List accumulatedEvents = new ArrayList(); + private List inFlightEvents = new ArrayList(); + private int numSkippedEventsDueToFullBuffer; + private AttributionIdentifiers attributionIdentifiers; + private String packageName; + private String hashedDeviceAndAppId; + + public static final String EVENT_COUNT_KEY = "event_count"; + public static final String ENCODED_EVENTS_KEY = "encoded_events"; + public static final String NUM_SKIPPED_KEY = "num_skipped"; + + private final int MAX_ACCUMULATED_LOG_EVENTS = 1000; + + public SessionEventsState(AttributionIdentifiers identifiers, String packageName, String hashedDeviceAndAppId) { + this.attributionIdentifiers = identifiers; + this.packageName = packageName; + this.hashedDeviceAndAppId = hashedDeviceAndAppId; + } + + // Synchronize here and in other methods on this class, because could be coming in from different + // AppEventsLoggers on different threads pointing at the same session. + public synchronized void addEvent(AppEvent event) { + if (accumulatedEvents.size() + inFlightEvents.size() >= MAX_ACCUMULATED_LOG_EVENTS) { + numSkippedEventsDueToFullBuffer++; + } else { + accumulatedEvents.add(event); + } + } + + public synchronized int getAccumulatedEventCount() { + return accumulatedEvents.size(); + } + + public synchronized void clearInFlightAndStats(boolean moveToAccumulated) { + if (moveToAccumulated) { + accumulatedEvents.addAll(inFlightEvents); + } + inFlightEvents.clear(); + numSkippedEventsDueToFullBuffer = 0; + } + + public int populateRequest(Request request, boolean includeImplicitEvents, + boolean includeAttribution, boolean limitEventUsage) { + + int numSkipped; + JSONArray jsonArray; + synchronized (this) { + numSkipped = numSkippedEventsDueToFullBuffer; + + // move all accumulated events to inFlight. + inFlightEvents.addAll(accumulatedEvents); + accumulatedEvents.clear(); + + jsonArray = new JSONArray(); + for (AppEvent event : inFlightEvents) { + if (includeImplicitEvents || !event.getIsImplicit()) { + jsonArray.put(event.getJSONObject()); + } + } + + if (jsonArray.length() == 0) { + return 0; + } + } + + populateRequest(request, numSkipped, jsonArray, includeAttribution, limitEventUsage); + return jsonArray.length(); + } + + public synchronized List getEventsToPersist() { + // We will only persist accumulated events, not ones currently in-flight. This means if an in-flight + // request fails, those requests will not be persisted and thus might be lost if the process terminates + // while the flush is in progress. + List result = accumulatedEvents; + accumulatedEvents = new ArrayList(); + return result; + } + + public synchronized void accumulatePersistedEvents(List events) { + // We won't skip events due to a full buffer, since we already accumulated them once and persisted + // them. But they will count against the buffer size when further events are accumulated. + accumulatedEvents.addAll(events); + } + + private void populateRequest(Request request, int numSkipped, JSONArray events, boolean includeAttribution, + boolean limitEventUsage) { + GraphObject publishParams = GraphObject.Factory.create(); + publishParams.setProperty("event", "CUSTOM_APP_EVENTS"); + + if (numSkippedEventsDueToFullBuffer > 0) { + publishParams.setProperty("num_skipped_events", numSkipped); + } + + if (includeAttribution) { + Utility.setAppEventAttributionParameters(publishParams, attributionIdentifiers, + hashedDeviceAndAppId, limitEventUsage); + } + + publishParams.setProperty("application_package_name", packageName); + + request.setGraphObject(publishParams); + + Bundle requestParameters = request.getParameters(); + if (requestParameters == null) { + requestParameters = new Bundle(); + } + + String jsonString = events.toString(); + if (jsonString != null) { + requestParameters.putByteArray("custom_events_file", getStringAsByteArray(jsonString)); + request.setTag(jsonString); + } + request.setParameters(requestParameters); + } + + private byte[] getStringAsByteArray(String jsonString) { + byte[] jsonUtf8 = null; + try { + jsonUtf8 = jsonString.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // shouldn't happen, but just in case: + Utility.logd("Encoding exception: ", e); + } + return jsonUtf8; + } + } + + static class AppEvent implements Serializable { + private static final long serialVersionUID = 1L; + + private JSONObject jsonObject; + private boolean isImplicit; + private static final HashSet validatedIdentifiers = new HashSet(); + private String name; + + public AppEvent(String eventName, Double valueToSum, Bundle parameters, boolean isImplicitlyLogged) { + + validateIdentifier(eventName); + + this.name = eventName; + + isImplicit = isImplicitlyLogged; + jsonObject = new JSONObject(); + + try { + + jsonObject.put("_eventName", eventName); + jsonObject.put("_logTime", System.currentTimeMillis() / 1000); + + if (valueToSum != null) { + jsonObject.put("_valueToSum", valueToSum.doubleValue()); + } + + if (isImplicit) { + jsonObject.put("_implicitlyLogged", "1"); + } + + String appVersion = Settings.getAppVersion(); + if (appVersion != null) { + jsonObject.put("_appVersion", appVersion); + } + + if (parameters != null) { + for (String key : parameters.keySet()) { + + validateIdentifier(key); + + Object value = parameters.get(key); + if (!(value instanceof String) && !(value instanceof Number)) { + throw new FacebookException( + String.format("Parameter value '%s' for key '%s' should be a string or a numeric type.", + value, key)); + } + + jsonObject.put(key, value.toString()); + } + } + + if (!isImplicit) { + Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents", + "Created app event '%s'", jsonObject.toString()); + } + } catch (JSONException jsonException) { + + // If any of the above failed, just consider this an illegal event. + Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents", + "JSON encoding for app event failed: '%s'", jsonException.toString()); + jsonObject = null; + + } + } + + public String getName() { + return name; + } + + private AppEvent(String jsonString, boolean isImplicit) throws JSONException { + jsonObject = new JSONObject(jsonString); + this.isImplicit = isImplicit; + } + + public boolean getIsImplicit() { return isImplicit; } + + public JSONObject getJSONObject() { + return jsonObject; + } + + // throw exception if not valid. + private void validateIdentifier(String identifier) { + + // Identifier should be 40 chars or less, and only have 0-9A-Za-z, underscore, hyphen, and space (but no + // hyphen or space in the first position). + final String regex = "^[0-9a-zA-Z_]+[0-9a-zA-Z _-]*$"; + + final int MAX_IDENTIFIER_LENGTH = 40; + if (identifier == null || identifier.length() == 0 || identifier.length() > MAX_IDENTIFIER_LENGTH) { + if (identifier == null) { + identifier = ""; + } + throw new FacebookException( + String.format("Identifier '%s' must be less than %d characters", identifier, MAX_IDENTIFIER_LENGTH)); + } + + boolean alreadyValidated = false; + synchronized (validatedIdentifiers) { + alreadyValidated = validatedIdentifiers.contains(identifier); + } + + if (!alreadyValidated) { + if (identifier.matches(regex)) { + synchronized (validatedIdentifiers) { + validatedIdentifiers.add(identifier); + } + } else { + throw new FacebookException( + String.format("Skipping event named '%s' due to illegal name - must be under 40 chars " + + "and alphanumeric, _, - or space, and not start with a space or hyphen.", + identifier)); + } + } + + } + + private static class SerializationProxyV1 implements Serializable { + private static final long serialVersionUID = -2488473066578201069L; + private final String jsonString; + private final boolean isImplicit; + + private SerializationProxyV1(String jsonString, boolean isImplicit) { + this.jsonString = jsonString; + this.isImplicit = isImplicit; + } + + private Object readResolve() throws JSONException { + return new AppEvent(jsonString, isImplicit); + } + } + + private Object writeReplace() { + return new SerializationProxyV1(jsonObject.toString(), isImplicit); + } + + @Override + public String toString() { + return String.format("\"%s\", implicit: %b, json: %s", jsonObject.optString("_eventName"), + isImplicit, jsonObject.toString()); + } + } + + // Read/write operations are thread-safe/atomic across all instances of PersistedEvents, but modifications + // to any individual instance are not thread-safe. + static class PersistedEvents { + static final String PERSISTED_EVENTS_FILENAME = "AppEventsLogger.persistedevents"; + + private static Object staticLock = new Object(); + + private Context context; + private HashMap> persistedEvents = + new HashMap>(); + + private PersistedEvents(Context context) { + this.context = context; + } + + public static PersistedEvents readAndClearStore(Context context) { + synchronized (staticLock) { + PersistedEvents persistedEvents = new PersistedEvents(context); + + persistedEvents.readAndClearStore(); + + return persistedEvents; + } + } + public static void persistEvents(Context context, AccessTokenAppIdPair accessTokenAppId, + SessionEventsState eventsToPersist) { + Map map = new HashMap(); + map.put(accessTokenAppId, eventsToPersist); + persistEvents(context, map); + } + + public static void persistEvents(Context context, + Map eventsToPersist) { + synchronized (staticLock) { + // Note that we don't track which instance of AppEventsLogger added a particular event to + // SessionEventsState; when a particular Context is being destroyed, we'll persist all accumulated + // events. More sophisticated tracking could be done to try to reduce unnecessary persisting of events, + // but the overall number of events is not expected to be large. + PersistedEvents persistedEvents = readAndClearStore(context); + + for (Map.Entry entry : eventsToPersist.entrySet()) { + List events = entry.getValue().getEventsToPersist(); + if (events.size() == 0) { + continue; + } + + persistedEvents.addEvents(entry.getKey(), events); + } + + persistedEvents.write(); + } + } + + public Set keySet() { + return persistedEvents.keySet(); + } + + public List getEvents(AccessTokenAppIdPair accessTokenAppId) { + return persistedEvents.get(accessTokenAppId); + } + + private void write() { + ObjectOutputStream oos = null; + try { + oos = new ObjectOutputStream( + new BufferedOutputStream(context.openFileOutput(PERSISTED_EVENTS_FILENAME, 0))); + oos.writeObject(persistedEvents); + } catch (Exception e) { + Log.d(TAG, "Got unexpected exception: " + e.toString()); + } finally { + Utility.closeQuietly(oos); + } + } + + private void readAndClearStore() { + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream( + new BufferedInputStream(context.openFileInput(PERSISTED_EVENTS_FILENAME))); + + @SuppressWarnings("unchecked") + HashMap> obj = + (HashMap>) ois.readObject(); + + // Note: We delete the store before we store the events; this means we'd prefer to lose some + // events in the case of exception rather than potentially log them twice. + context.getFileStreamPath(PERSISTED_EVENTS_FILENAME).delete(); + persistedEvents = obj; + } catch (FileNotFoundException e) { + // Expected if we never persisted any events. + } catch (Exception e) { + Log.d(TAG, "Got unexpected exception: " + e.toString()); + } finally { + Utility.closeQuietly(ois); + } + } + + public void addEvents(AccessTokenAppIdPair accessTokenAppId, List eventsToPersist) { + if (!persistedEvents.containsKey(accessTokenAppId)) { + persistedEvents.put(accessTokenAppId, new ArrayList()); + } + persistedEvents.get(accessTokenAppId).addAll(eventsToPersist); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppLinkData.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppLinkData.java new file mode 100644 index 0000000000..fb03e843ba --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AppLinkData.java @@ -0,0 +1,407 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import com.facebook.internal.*; +import com.facebook.model.GraphObject; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Iterator; + +/** + * Class to encapsulate an app link, and provide methods for constructing the data from various sources + */ +public class AppLinkData { + + /** + * Key that should be used to pull out the UTC Unix tap-time from the arguments for this app link. + */ + public static final String ARGUMENTS_TAPTIME_KEY = "com.facebook.platform.APPLINK_TAP_TIME_UTC"; + /** + * Key that should be used to get the "referer_data" field for this app link. + */ + public static final String ARGUMENTS_REFERER_DATA_KEY = "referer_data"; + + /** + * Key that should be used to pull out the native class that would have been used if the applink was deferred. + */ + public static final String ARGUMENTS_NATIVE_CLASS_KEY = "com.facebook.platform.APPLINK_NATIVE_CLASS"; + + /** + * Key that should be used to pull out the native url that would have been used if the applink was deferred. + */ + public static final String ARGUMENTS_NATIVE_URL = "com.facebook.platform.APPLINK_NATIVE_URL"; + + static final String BUNDLE_APPLINK_ARGS_KEY = "com.facebook.platform.APPLINK_ARGS"; + private static final String BUNDLE_AL_APPLINK_DATA_KEY = "al_applink_data"; + private static final String APPLINK_BRIDGE_ARGS_KEY = "bridge_args"; + private static final String APPLINK_METHOD_ARGS_KEY = "method_args"; + private static final String APPLINK_VERSION_KEY = "version"; + private static final String BRIDGE_ARGS_METHOD_KEY = "method"; + private static final String DEFERRED_APP_LINK_EVENT = "DEFERRED_APP_LINK"; + private static final String DEFERRED_APP_LINK_PATH = "%s/activities"; + + private static final String DEFERRED_APP_LINK_ARGS_FIELD = "applink_args"; + private static final String DEFERRED_APP_LINK_CLASS_FIELD = "applink_class"; + private static final String DEFERRED_APP_LINK_CLICK_TIME_FIELD = "click_time"; + private static final String DEFERRED_APP_LINK_URL_FIELD = "applink_url"; + + private static final String METHOD_ARGS_TARGET_URL_KEY = "target_url"; + private static final String METHOD_ARGS_REF_KEY = "ref"; + private static final String REFERER_DATA_REF_KEY = "fb_ref"; + private static final String TAG = AppLinkData.class.getCanonicalName(); + + private String ref; + private Uri targetUri; + private JSONObject arguments; + private Bundle argumentBundle; + + /** + * Asynchronously fetches app link information that might have been stored for use + * after installation of the app + * @param context The context + * @param completionHandler CompletionHandler to be notified with the AppLinkData object or null if none is + * available. Must not be null. + */ + public static void fetchDeferredAppLinkData(Context context, CompletionHandler completionHandler) { + fetchDeferredAppLinkData(context, null, completionHandler); + } + + /** + * Asynchronously fetches app link information that might have been stored for use + * after installation of the app + * @param context The context + * @param applicationId Facebook application Id. If null, it is taken from the manifest + * @param completionHandler CompletionHandler to be notified with the AppLinkData object or null if none is + * available. Must not be null. + */ + public static void fetchDeferredAppLinkData( + Context context, + String applicationId, + final CompletionHandler completionHandler) { + Validate.notNull(context, "context"); + Validate.notNull(completionHandler, "completionHandler"); + + if (applicationId == null) { + applicationId = Utility.getMetadataApplicationId(context); + } + + Validate.notNull(applicationId, "applicationId"); + + final Context applicationContext = context.getApplicationContext(); + final String applicationIdCopy = applicationId; + Settings.getExecutor().execute(new Runnable() { + @Override + public void run() { + fetchDeferredAppLinkFromServer(applicationContext, applicationIdCopy, completionHandler); + } + }); + } + + private static void fetchDeferredAppLinkFromServer( + Context context, + String applicationId, + final CompletionHandler completionHandler) { + + GraphObject deferredApplinkParams = GraphObject.Factory.create(); + deferredApplinkParams.setProperty("event", DEFERRED_APP_LINK_EVENT); + Utility.setAppEventAttributionParameters(deferredApplinkParams, + AttributionIdentifiers.getAttributionIdentifiers(context), + Utility.getHashedDeviceAndAppID(context, applicationId), + Settings.getLimitEventAndDataUsage(context)); + deferredApplinkParams.setProperty("application_package_name", context.getPackageName()); + + String deferredApplinkUrlPath = String.format(DEFERRED_APP_LINK_PATH, applicationId); + AppLinkData appLinkData = null; + + try { + Request deferredApplinkRequest = Request.newPostRequest( + null, deferredApplinkUrlPath, deferredApplinkParams, null); + Response deferredApplinkResponse = deferredApplinkRequest.executeAndWait(); + GraphObject wireResponse = deferredApplinkResponse.getGraphObject(); + JSONObject jsonResponse = wireResponse != null ? wireResponse.getInnerJSONObject() : null; + if (jsonResponse != null) { + final String appLinkArgsJsonString = jsonResponse.optString(DEFERRED_APP_LINK_ARGS_FIELD); + final long tapTimeUtc = jsonResponse.optLong(DEFERRED_APP_LINK_CLICK_TIME_FIELD, -1); + final String appLinkClassName = jsonResponse.optString(DEFERRED_APP_LINK_CLASS_FIELD); + final String appLinkUrl = jsonResponse.optString(DEFERRED_APP_LINK_URL_FIELD); + + if (appLinkArgsJsonString != null && appLinkArgsJsonString != "") { + appLinkData = createFromJson(appLinkArgsJsonString); + + if (tapTimeUtc != -1) { + try { + if (appLinkData.arguments != null) { + appLinkData.arguments.put(ARGUMENTS_TAPTIME_KEY, tapTimeUtc); + } + if (appLinkData.argumentBundle != null) { + appLinkData.argumentBundle.putString(ARGUMENTS_TAPTIME_KEY, Long.toString(tapTimeUtc)); + } + } catch (JSONException e) { + Log.d(TAG, "Unable to put tap time in AppLinkData.arguments"); + } + } + + if (appLinkClassName != null) { + try { + if (appLinkData.arguments != null) { + appLinkData.arguments.put(ARGUMENTS_NATIVE_CLASS_KEY, appLinkClassName); + } + if (appLinkData.argumentBundle != null) { + appLinkData.argumentBundle.putString(ARGUMENTS_NATIVE_CLASS_KEY, appLinkClassName); + } + } catch (JSONException e) { + Log.d(TAG, "Unable to put tap time in AppLinkData.arguments"); + } + } + + if (appLinkUrl != null) { + try { + if (appLinkData.arguments != null) { + appLinkData.arguments.put(ARGUMENTS_NATIVE_URL, appLinkUrl); + } + if (appLinkData.argumentBundle != null) { + appLinkData.argumentBundle.putString(ARGUMENTS_NATIVE_URL, appLinkUrl); + } + } catch (JSONException e) { + Log.d(TAG, "Unable to put tap time in AppLinkData.arguments"); + } + } + } + } + } catch (Exception e) { + Utility.logd(TAG, "Unable to fetch deferred applink from server"); + } + + completionHandler.onDeferredAppLinkDataFetched(appLinkData); + } + + /** + * Parses out any app link data from the Intent of the Activity passed in. + * @param activity Activity that was started because of an app link + * @return AppLinkData if found. null if not. + */ + public static AppLinkData createFromActivity(Activity activity) { + Validate.notNull(activity, "activity"); + Intent intent = activity.getIntent(); + if (intent == null) { + return null; + } + + AppLinkData appLinkData = createFromAlApplinkData(intent); + if (appLinkData == null) { + String appLinkArgsJsonString = intent.getStringExtra(BUNDLE_APPLINK_ARGS_KEY); + appLinkData = createFromJson(appLinkArgsJsonString); + } + if (appLinkData == null) { + // Try regular app linking + appLinkData = createFromUri(intent.getData()); + } + + return appLinkData; + } + + private static AppLinkData createFromAlApplinkData(Intent intent) { + Bundle applinks = intent.getBundleExtra(BUNDLE_AL_APPLINK_DATA_KEY); + if (applinks == null) { + return null; + } + + AppLinkData appLinkData = new AppLinkData(); + appLinkData.targetUri = intent.getData(); + if (appLinkData.targetUri == null) { + String targetUriString = applinks.getString(METHOD_ARGS_TARGET_URL_KEY); + if (targetUriString != null) { + appLinkData.targetUri = Uri.parse(targetUriString); + } + } + appLinkData.argumentBundle = applinks; + appLinkData.arguments = null; + Bundle refererData = applinks.getBundle(ARGUMENTS_REFERER_DATA_KEY); + if (refererData != null) { + appLinkData.ref = refererData.getString(REFERER_DATA_REF_KEY); + } + + return appLinkData; + } + + private static AppLinkData createFromJson(String jsonString) { + if (jsonString == null) { + return null; + } + + try { + // Any missing or malformed data will result in a JSONException + JSONObject appLinkArgsJson = new JSONObject(jsonString); + String version = appLinkArgsJson.getString(APPLINK_VERSION_KEY); + + JSONObject bridgeArgs = appLinkArgsJson.getJSONObject(APPLINK_BRIDGE_ARGS_KEY); + String method = bridgeArgs.getString(BRIDGE_ARGS_METHOD_KEY); + if (method.equals("applink") && version.equals("2")) { + // We have a new deep link + AppLinkData appLinkData = new AppLinkData(); + + appLinkData.arguments = appLinkArgsJson.getJSONObject(APPLINK_METHOD_ARGS_KEY); + // first look for the "ref" key in the top level args + if (appLinkData.arguments.has(METHOD_ARGS_REF_KEY)) { + appLinkData.ref = appLinkData.arguments.getString(METHOD_ARGS_REF_KEY); + } else if (appLinkData.arguments.has(ARGUMENTS_REFERER_DATA_KEY)) { + // if it's not in the top level args, it could be in the "referer_data" blob + JSONObject refererData = appLinkData.arguments.getJSONObject(ARGUMENTS_REFERER_DATA_KEY); + if (refererData.has(REFERER_DATA_REF_KEY)) { + appLinkData.ref = refererData.getString(REFERER_DATA_REF_KEY); + } + } + + if (appLinkData.arguments.has(METHOD_ARGS_TARGET_URL_KEY)) { + appLinkData.targetUri = Uri.parse(appLinkData.arguments.getString(METHOD_ARGS_TARGET_URL_KEY)); + } + + appLinkData.argumentBundle = toBundle(appLinkData.arguments); + + return appLinkData; + } + } catch (JSONException e) { + Log.d(TAG, "Unable to parse AppLink JSON", e); + } catch (FacebookException e) { + Log.d(TAG, "Unable to parse AppLink JSON", e); + } + + return null; + } + + private static AppLinkData createFromUri(Uri appLinkDataUri) { + if (appLinkDataUri == null) { + return null; + } + + AppLinkData appLinkData = new AppLinkData(); + appLinkData.targetUri = appLinkDataUri; + return appLinkData; + } + + private static Bundle toBundle(JSONObject node) throws JSONException { + Bundle bundle = new Bundle(); + @SuppressWarnings("unchecked") + Iterator fields = node.keys(); + while (fields.hasNext()) { + String key = fields.next(); + Object value; + value = node.get(key); + + if (value instanceof JSONObject) { + bundle.putBundle(key, toBundle((JSONObject) value)); + } else if (value instanceof JSONArray) { + JSONArray valueArr = (JSONArray) value; + if (valueArr.length() == 0) { + bundle.putStringArray(key, new String[0]); + } else { + Object firstNode = valueArr.get(0); + if (firstNode instanceof JSONObject) { + Bundle[] bundles = new Bundle[valueArr.length()]; + for (int i = 0; i < valueArr.length(); i++) { + bundles[i] = toBundle(valueArr.getJSONObject(i)); + } + bundle.putParcelableArray(key, bundles); + } else if (firstNode instanceof JSONArray) { + // we don't support nested arrays + throw new FacebookException("Nested arrays are not supported."); + } else { // just use the string value + String[] arrValues = new String[valueArr.length()]; + for (int i = 0; i < valueArr.length(); i++) { + arrValues[i] = valueArr.get(i).toString(); + } + bundle.putStringArray(key, arrValues); + } + } + } else { + bundle.putString(key, value.toString()); + } + } + return bundle; + } + + + private AppLinkData() { + } + + /** + * Returns the target uri for this App Link. + * @return target uri + */ + public Uri getTargetUri() { + return targetUri; + } + + /** + * Returns the ref for this App Link. + * @return ref + */ + public String getRef() { + return ref; + } + + /** + * This method has been deprecated. Please use {@link AppLinkData#getArgumentBundle()} instead. + * @return JSONObject property bag. + */ + @Deprecated + public JSONObject getArguments() { + return arguments; + } + + /** + * The full set of arguments for this app link. Properties like target uri & ref are typically + * picked out of this set of arguments. + * @return App link related arguments as a bundle. + */ + public Bundle getArgumentBundle() { + return argumentBundle; + } + + /** + * The referer data associated with the app link. This will contain Facebook specific information like + * fb_access_token, fb_expires_in, and fb_ref. + * @return the referer data. + */ + public Bundle getRefererData() { + if (argumentBundle != null) { + return argumentBundle.getBundle(ARGUMENTS_REFERER_DATA_KEY); + } + return null; + } + + /** + * Interface to asynchronously receive AppLinkData after it has been fetched. + */ + public interface CompletionHandler { + /** + * This method is called when deferred app link data has been fetched. If no app link data was found, + * this method is called with null + * @param appLinkData The app link data that was fetched. Null if none was found. + */ + void onDeferredAppLinkDataFetched(AppLinkData appLinkData); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/AuthorizationClient.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AuthorizationClient.java new file mode 100644 index 0000000000..70ac8a1038 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/AuthorizationClient.java @@ -0,0 +1,1073 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.Manifest; +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.TextUtils; +import android.webkit.CookieSyncManager; +import com.facebook.android.R; +import com.facebook.internal.AnalyticsEvents; +import com.facebook.internal.NativeProtocol; +import com.facebook.internal.ServerProtocol; +import com.facebook.internal.Utility; +import com.facebook.model.GraphUser; +import com.facebook.widget.WebDialog; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.Serializable; +import java.util.*; + +class AuthorizationClient implements Serializable { + private static final long serialVersionUID = 1L; + private static final String TAG = "Facebook-AuthorizationClient"; + private static final String WEB_VIEW_AUTH_HANDLER_STORE = + "com.facebook.AuthorizationClient.WebViewAuthHandler.TOKEN_STORE_KEY"; + private static final String WEB_VIEW_AUTH_HANDLER_TOKEN_KEY = "TOKEN"; + + // Constants for logging login-related data. Some of these are only used by Session, but grouped here for + // maintainability. + private static final String EVENT_NAME_LOGIN_METHOD_START = "fb_mobile_login_method_start"; + private static final String EVENT_NAME_LOGIN_METHOD_COMPLETE = "fb_mobile_login_method_complete"; + private static final String EVENT_PARAM_METHOD_RESULT_SKIPPED = "skipped"; + static final String EVENT_NAME_LOGIN_START = "fb_mobile_login_start"; + static final String EVENT_NAME_LOGIN_COMPLETE = "fb_mobile_login_complete"; + // Note: to ensure stability of column mappings across the four different event types, we prepend a column + // index to each name, and we log all columns with all events, even if they are empty. + static final String EVENT_PARAM_AUTH_LOGGER_ID = "0_auth_logger_id"; + static final String EVENT_PARAM_TIMESTAMP = "1_timestamp_ms"; + static final String EVENT_PARAM_LOGIN_RESULT = "2_result"; + static final String EVENT_PARAM_METHOD = "3_method"; + static final String EVENT_PARAM_ERROR_CODE = "4_error_code"; + static final String EVENT_PARAM_ERROR_MESSAGE = "5_error_message"; + static final String EVENT_PARAM_EXTRAS = "6_extras"; + static final String EVENT_EXTRAS_TRY_LOGIN_ACTIVITY = "try_login_activity"; + static final String EVENT_EXTRAS_TRY_LEGACY = "try_legacy"; + static final String EVENT_EXTRAS_LOGIN_BEHAVIOR = "login_behavior"; + static final String EVENT_EXTRAS_REQUEST_CODE = "request_code"; + static final String EVENT_EXTRAS_IS_LEGACY = "is_legacy"; + static final String EVENT_EXTRAS_PERMISSIONS = "permissions"; + static final String EVENT_EXTRAS_DEFAULT_AUDIENCE = "default_audience"; + static final String EVENT_EXTRAS_MISSING_INTERNET_PERMISSION = "no_internet_permission"; + static final String EVENT_EXTRAS_NOT_TRIED = "not_tried"; + static final String EVENT_EXTRAS_NEW_PERMISSIONS = "new_permissions"; + static final String EVENT_EXTRAS_SERVICE_DISABLED = "service_disabled"; + static final String EVENT_EXTRAS_APP_CALL_ID = "call_id"; + static final String EVENT_EXTRAS_PROTOCOL_VERSION = "protocol_version"; + static final String EVENT_EXTRAS_WRITE_PRIVACY = "write_privacy"; + + List handlersToTry; + AuthHandler currentHandler; + transient Context context; + transient StartActivityDelegate startActivityDelegate; + transient OnCompletedListener onCompletedListener; + transient BackgroundProcessingListener backgroundProcessingListener; + transient boolean checkedInternetPermission; + AuthorizationRequest pendingRequest; + Map loggingExtras; + private transient AppEventsLogger appEventsLogger; + + interface OnCompletedListener { + void onCompleted(Result result); + } + + interface BackgroundProcessingListener { + void onBackgroundProcessingStarted(); + + void onBackgroundProcessingStopped(); + } + + interface StartActivityDelegate { + public void startActivityForResult(Intent intent, int requestCode); + + public Activity getActivityContext(); + } + + void setContext(final Context context) { + this.context = context; + // We rely on individual requests to tell us how to start an activity. + startActivityDelegate = null; + } + + void setContext(final Activity activity) { + this.context = activity; + + // If we are used in the context of an activity, we will always use that activity to + // call startActivityForResult. + startActivityDelegate = new StartActivityDelegate() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { + activity.startActivityForResult(intent, requestCode); + } + + @Override + public Activity getActivityContext() { + return activity; + } + }; + } + + void startOrContinueAuth(AuthorizationRequest request) { + if (getInProgress()) { + continueAuth(); + } else { + authorize(request); + } + } + + void authorize(AuthorizationRequest request) { + if (request == null) { + return; + } + + if (pendingRequest != null) { + throw new FacebookException("Attempted to authorize while a request is pending."); + } + + if (request.needsNewTokenValidation() && !checkInternetPermission()) { + // We're going to need INTERNET permission later and don't have it, so fail early. + return; + } + pendingRequest = request; + handlersToTry = getHandlerTypes(request); + tryNextHandler(); + } + + void continueAuth() { + if (pendingRequest == null || currentHandler == null) { + throw new FacebookException("Attempted to continue authorization without a pending request."); + } + + if (currentHandler.needsRestart()) { + currentHandler.cancel(); + tryCurrentHandler(); + } + } + + boolean getInProgress() { + return pendingRequest != null && currentHandler != null; + } + + void cancelCurrentHandler() { + if (currentHandler != null) { + currentHandler.cancel(); + } + } + + boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == pendingRequest.getRequestCode()) { + return currentHandler.onActivityResult(requestCode, resultCode, data); + } + return false; + } + + private List getHandlerTypes(AuthorizationRequest request) { + ArrayList handlers = new ArrayList(); + + final SessionLoginBehavior behavior = request.getLoginBehavior(); + if (behavior.allowsKatanaAuth()) { + if (!request.isLegacy()) { + handlers.add(new GetTokenAuthHandler()); + } + handlers.add(new KatanaProxyAuthHandler()); + } + + if (behavior.allowsWebViewAuth()) { + handlers.add(new WebViewAuthHandler()); + } + + return handlers; + } + + boolean checkInternetPermission() { + if (checkedInternetPermission) { + return true; + } + + int permissionCheck = checkPermission(Manifest.permission.INTERNET); + if (permissionCheck != PackageManager.PERMISSION_GRANTED) { + String errorType = context.getString(R.string.com_facebook_internet_permission_error_title); + String errorDescription = context.getString(R.string.com_facebook_internet_permission_error_message); + complete(Result.createErrorResult(pendingRequest, errorType, errorDescription)); + + return false; + } + + checkedInternetPermission = true; + return true; + } + + void tryNextHandler() { + if (currentHandler != null) { + logAuthorizationMethodComplete(currentHandler.getNameForLogging(), EVENT_PARAM_METHOD_RESULT_SKIPPED, + null, null, currentHandler.methodLoggingExtras); + } + + while (handlersToTry != null && !handlersToTry.isEmpty()) { + currentHandler = handlersToTry.remove(0); + + boolean started = tryCurrentHandler(); + + if (started) { + return; + } + } + + if (pendingRequest != null) { + // We went through all handlers without successfully attempting an auth. + completeWithFailure(); + } + } + + private void completeWithFailure() { + complete(Result.createErrorResult(pendingRequest, "Login attempt failed.", null)); + } + + private void addLoggingExtra(String key, String value, boolean accumulate) { + if (loggingExtras == null) { + loggingExtras = new HashMap(); + } + if (loggingExtras.containsKey(key) && accumulate) { + value = loggingExtras.get(key) + "," + value; + } + loggingExtras.put(key, value); + } + + boolean tryCurrentHandler() { + if (currentHandler.needsInternetPermission() && !checkInternetPermission()) { + addLoggingExtra(EVENT_EXTRAS_MISSING_INTERNET_PERMISSION, AppEventsConstants.EVENT_PARAM_VALUE_YES, + false); + return false; + } + + boolean tried = currentHandler.tryAuthorize(pendingRequest); + if (tried) { + logAuthorizationMethodStart(currentHandler.getNameForLogging()); + } else { + // We didn't try it, so we don't get any other completion notification -- log that we skipped it. + addLoggingExtra(EVENT_EXTRAS_NOT_TRIED, currentHandler.getNameForLogging(), true); + } + + return tried; + } + + void completeAndValidate(Result outcome) { + // Do we need to validate a successful result (as in the case of a reauth)? + if (outcome.token != null && pendingRequest.needsNewTokenValidation()) { + validateSameFbidAndFinish(outcome); + } else { + // We're done, just notify the listener. + complete(outcome); + } + } + + void complete(Result outcome) { + // This might be null if, for some reason, none of the handlers were successfully tried (in which case + // we already logged that). + if (currentHandler != null) { + logAuthorizationMethodComplete(currentHandler.getNameForLogging(), outcome, + currentHandler.methodLoggingExtras); + } + + if (loggingExtras != null) { + // Pass this back to the caller for logging at the aggregate level. + outcome.loggingExtras = loggingExtras; + } + + handlersToTry = null; + currentHandler = null; + pendingRequest = null; + loggingExtras = null; + + notifyOnCompleteListener(outcome); + } + + OnCompletedListener getOnCompletedListener() { + return onCompletedListener; + } + + void setOnCompletedListener(OnCompletedListener onCompletedListener) { + this.onCompletedListener = onCompletedListener; + } + + BackgroundProcessingListener getBackgroundProcessingListener() { + return backgroundProcessingListener; + } + + void setBackgroundProcessingListener(BackgroundProcessingListener backgroundProcessingListener) { + this.backgroundProcessingListener = backgroundProcessingListener; + } + + StartActivityDelegate getStartActivityDelegate() { + if (startActivityDelegate != null) { + return startActivityDelegate; + } else if (pendingRequest != null) { + // Wrap the request's delegate in our own. + return new StartActivityDelegate() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { + pendingRequest.getStartActivityDelegate().startActivityForResult(intent, requestCode); + } + + @Override + public Activity getActivityContext() { + return pendingRequest.getStartActivityDelegate().getActivityContext(); + } + }; + } + return null; + } + + int checkPermission(String permission) { + return context.checkCallingOrSelfPermission(permission); + } + + void validateSameFbidAndFinish(Result pendingResult) { + if (pendingResult.token == null) { + throw new FacebookException("Can't validate without a token"); + } + + RequestBatch batch = createReauthValidationBatch(pendingResult); + + notifyBackgroundProcessingStart(); + + batch.executeAsync(); + } + + RequestBatch createReauthValidationBatch(final Result pendingResult) { + // We need to ensure that the token we got represents the same fbid as the old one. We issue + // a "me" request using the current token, a "me" request using the new token, and a "me/permissions" + // request using the current token to get the permissions of the user. + + final ArrayList fbids = new ArrayList(); + final ArrayList grantedPermissions = new ArrayList(); + final ArrayList declinedPermissions = new ArrayList(); + final String newToken = pendingResult.token.getToken(); + + Request.Callback meCallback = new Request.Callback() { + @Override + public void onCompleted(Response response) { + try { + GraphUser user = response.getGraphObjectAs(GraphUser.class); + if (user != null) { + fbids.add(user.getId()); + } + } catch (Exception ex) { + } + } + }; + + String validateSameFbidAsToken = pendingRequest.getPreviousAccessToken(); + Request requestCurrentTokenMe = createGetProfileIdRequest(validateSameFbidAsToken); + requestCurrentTokenMe.setCallback(meCallback); + + Request requestNewTokenMe = createGetProfileIdRequest(newToken); + requestNewTokenMe.setCallback(meCallback); + + Request requestCurrentTokenPermissions = createGetPermissionsRequest(validateSameFbidAsToken); + requestCurrentTokenPermissions.setCallback(new Request.Callback() { + @Override + public void onCompleted(Response response) { + try { + Session.PermissionsPair permissionsPair = Session.handlePermissionResponse(response); + if (permissionsPair != null) { + grantedPermissions.addAll(permissionsPair.getGrantedPermissions()); + declinedPermissions.addAll(permissionsPair.getDeclinedPermissions()); + } + } catch (Exception ex) { + } + } + }); + + RequestBatch batch = new RequestBatch(requestCurrentTokenMe, requestNewTokenMe, + requestCurrentTokenPermissions); + batch.setBatchApplicationId(pendingRequest.getApplicationId()); + batch.addCallback(new RequestBatch.Callback() { + @Override + public void onBatchCompleted(RequestBatch batch) { + try { + Result result = null; + if (fbids.size() == 2 && fbids.get(0) != null && fbids.get(1) != null && + fbids.get(0).equals(fbids.get(1))) { + // Modify the token to have the right permission set. + AccessToken tokenWithPermissions = AccessToken + .createFromTokenWithRefreshedPermissions(pendingResult.token, + grantedPermissions, declinedPermissions); + result = Result.createTokenResult(pendingRequest, tokenWithPermissions); + } else { + result = Result + .createErrorResult(pendingRequest, "User logged in as different Facebook user.", null); + } + complete(result); + } catch (Exception ex) { + complete(Result.createErrorResult(pendingRequest, "Caught exception", ex.getMessage())); + } finally { + notifyBackgroundProcessingStop(); + } + } + }); + + return batch; + } + + Request createGetPermissionsRequest(String accessToken) { + Bundle parameters = new Bundle(); + parameters.putString("access_token", accessToken); + return new Request(null, "me/permissions", parameters, HttpMethod.GET, null); + } + + Request createGetProfileIdRequest(String accessToken) { + Bundle parameters = new Bundle(); + parameters.putString("fields", "id"); + parameters.putString("access_token", accessToken); + return new Request(null, "me", parameters, HttpMethod.GET, null); + } + + private AppEventsLogger getAppEventsLogger() { + if (appEventsLogger == null || appEventsLogger.getApplicationId() != pendingRequest.getApplicationId()) { + appEventsLogger = AppEventsLogger.newLogger(context, pendingRequest.getApplicationId()); + } + return appEventsLogger; + } + + private void notifyOnCompleteListener(Result outcome) { + if (onCompletedListener != null) { + onCompletedListener.onCompleted(outcome); + } + } + + private void notifyBackgroundProcessingStart() { + if (backgroundProcessingListener != null) { + backgroundProcessingListener.onBackgroundProcessingStarted(); + } + } + + private void notifyBackgroundProcessingStop() { + if (backgroundProcessingListener != null) { + backgroundProcessingListener.onBackgroundProcessingStopped(); + } + } + + private void logAuthorizationMethodStart(String method) { + Bundle bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId()); + bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); + bundle.putString(EVENT_PARAM_METHOD, method); + + getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_START, null, bundle); + } + + private void logAuthorizationMethodComplete(String method, Result result, Map loggingExtras) { + logAuthorizationMethodComplete(method, result.code.getLoggingValue(), result.errorMessage, result.errorCode, + loggingExtras); + } + + private void logAuthorizationMethodComplete(String method, String result, String errorMessage, String errorCode, + Map loggingExtras) { + Bundle bundle = null; + if (pendingRequest == null) { + // We don't expect this to happen, but if it does, log an event for diagnostic purposes. + bundle = newAuthorizationLoggingBundle(""); + bundle.putString(EVENT_PARAM_LOGIN_RESULT, Result.Code.ERROR.getLoggingValue()); + bundle.putString(EVENT_PARAM_ERROR_MESSAGE, + "Unexpected call to logAuthorizationMethodComplete with null pendingRequest."); + } else { + bundle = newAuthorizationLoggingBundle(pendingRequest.getAuthId()); + if (result != null) { + bundle.putString(EVENT_PARAM_LOGIN_RESULT, result); + } + if (errorMessage != null) { + bundle.putString(EVENT_PARAM_ERROR_MESSAGE, errorMessage); + } + if (errorCode != null) { + bundle.putString(EVENT_PARAM_ERROR_CODE, errorCode); + } + if (loggingExtras != null && !loggingExtras.isEmpty()) { + JSONObject jsonObject = new JSONObject(loggingExtras); + bundle.putString(EVENT_PARAM_EXTRAS, jsonObject.toString()); + } + } + bundle.putString(EVENT_PARAM_METHOD, method); + bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); + + getAppEventsLogger().logSdkEvent(EVENT_NAME_LOGIN_METHOD_COMPLETE, null, bundle); + } + + static Bundle newAuthorizationLoggingBundle(String authLoggerId) { + // We want to log all parameters for all events, to ensure stability of columns across different event types. + Bundle bundle = new Bundle(); + bundle.putLong(EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); + bundle.putString(EVENT_PARAM_AUTH_LOGGER_ID, authLoggerId); + bundle.putString(EVENT_PARAM_METHOD, ""); + bundle.putString(EVENT_PARAM_LOGIN_RESULT, ""); + bundle.putString(EVENT_PARAM_ERROR_MESSAGE, ""); + bundle.putString(EVENT_PARAM_ERROR_CODE, ""); + bundle.putString(EVENT_PARAM_EXTRAS, ""); + return bundle; + } + + abstract class AuthHandler implements Serializable { + private static final long serialVersionUID = 1L; + + Map methodLoggingExtras; + + abstract boolean tryAuthorize(AuthorizationRequest request); + abstract String getNameForLogging(); + + boolean onActivityResult(int requestCode, int resultCode, Intent data) { + return false; + } + + boolean needsRestart() { + return false; + } + + boolean needsInternetPermission() { + return false; + } + + void cancel() { + } + + protected void addLoggingExtra(String key, Object value) { + if (methodLoggingExtras == null) { + methodLoggingExtras = new HashMap(); + } + methodLoggingExtras.put(key, value == null ? null : value.toString()); + } + } + + class WebViewAuthHandler extends AuthHandler { + private static final long serialVersionUID = 1L; + private transient WebDialog loginDialog; + private String applicationId; + private String e2e; + + @Override + String getNameForLogging() { + return "web_view"; + } + + @Override + boolean needsRestart() { + // Because we are presenting WebView UI within the current context, we need to explicitly + // restart the process if the context goes away and is recreated. + return true; + } + + @Override + boolean needsInternetPermission() { + return true; + } + + @Override + void cancel() { + if (loginDialog != null) { + loginDialog.dismiss(); + loginDialog = null; + } + } + + @Override + boolean tryAuthorize(final AuthorizationRequest request) { + applicationId = request.getApplicationId(); + Bundle parameters = new Bundle(); + if (!Utility.isNullOrEmpty(request.getPermissions())) { + String scope = TextUtils.join(",", request.getPermissions()); + parameters.putString(ServerProtocol.DIALOG_PARAM_SCOPE, scope); + addLoggingExtra(ServerProtocol.DIALOG_PARAM_SCOPE, scope); + } + + String previousToken = request.getPreviousAccessToken(); + if (!Utility.isNullOrEmpty(previousToken) && (previousToken.equals(loadCookieToken()))) { + parameters.putString(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, previousToken); + // Don't log the actual access token, just its presence or absence. + addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_YES); + } else { + // The call to clear cookies will create the first instance of CookieSyncManager if necessary + Utility.clearFacebookCookies(context); + addLoggingExtra(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, AppEventsConstants.EVENT_PARAM_VALUE_NO); + } + + WebDialog.OnCompleteListener listener = new WebDialog.OnCompleteListener() { + @Override + public void onComplete(Bundle values, FacebookException error) { + onWebDialogComplete(request, values, error); + } + }; + + e2e = getE2E(); + addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e); + + WebDialog.Builder builder = + new AuthDialogBuilder(getStartActivityDelegate().getActivityContext(), applicationId, parameters) + .setE2E(e2e) + .setIsRerequest(request.isRerequest()) + .setOnCompleteListener(listener); + loginDialog = builder.build(); + loginDialog.show(); + + return true; + } + + void onWebDialogComplete(AuthorizationRequest request, Bundle values, + FacebookException error) { + Result outcome; + if (values != null) { + // Actual e2e we got from the dialog should be used for logging. + if (values.containsKey(ServerProtocol.DIALOG_PARAM_E2E)) { + e2e = values.getString(ServerProtocol.DIALOG_PARAM_E2E); + } + + AccessToken token = AccessToken + .createFromWebBundle(request.getPermissions(), values, AccessTokenSource.WEB_VIEW); + outcome = Result.createTokenResult(pendingRequest, token); + + // Ensure any cookies set by the dialog are saved + // This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager + // has never been created. + CookieSyncManager syncManager = CookieSyncManager.createInstance(context); + syncManager.sync(); + saveCookieToken(token.getToken()); + } else { + if (error instanceof FacebookOperationCanceledException) { + outcome = Result.createCancelResult(pendingRequest, "User canceled log in."); + } else { + // Something went wrong, don't log a completion event since it will skew timing results. + e2e = null; + + String errorCode = null; + String errorMessage = error.getMessage(); + if (error instanceof FacebookServiceException) { + FacebookRequestError requestError = ((FacebookServiceException)error).getRequestError(); + errorCode = String.format("%d", requestError.getErrorCode()); + errorMessage = requestError.toString(); + } + outcome = Result.createErrorResult(pendingRequest, null, errorMessage, errorCode); + } + } + + if (!Utility.isNullOrEmpty(e2e)) { + logWebLoginCompleted(applicationId, e2e); + } + + completeAndValidate(outcome); + } + + private void saveCookieToken(String token) { + Context context = getStartActivityDelegate().getActivityContext(); + SharedPreferences sharedPreferences = context.getSharedPreferences( + WEB_VIEW_AUTH_HANDLER_STORE, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, token); + if (!editor.commit()) { + Utility.logd(TAG, "Could not update saved web view auth handler token."); + } + } + + private String loadCookieToken() { + Context context = getStartActivityDelegate().getActivityContext(); + SharedPreferences sharedPreferences = context.getSharedPreferences( + WEB_VIEW_AUTH_HANDLER_STORE, + Context.MODE_PRIVATE); + return sharedPreferences.getString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, ""); + } + } + + class GetTokenAuthHandler extends AuthHandler { + private static final long serialVersionUID = 1L; + private transient GetTokenClient getTokenClient; + + @Override + String getNameForLogging() { + return "get_token"; + } + + @Override + void cancel() { + if (getTokenClient != null) { + getTokenClient.cancel(); + getTokenClient = null; + } + } + + @Override + boolean needsRestart() { + // if the getTokenClient is null, that means an orientation change has occurred, and we need + // to recreate the GetTokenClient, so return true to indicate we need a restart + return getTokenClient == null; + } + + boolean tryAuthorize(final AuthorizationRequest request) { + getTokenClient = new GetTokenClient(context, request.getApplicationId()); + if (!getTokenClient.start()) { + return false; + } + + notifyBackgroundProcessingStart(); + + GetTokenClient.CompletedListener callback = new GetTokenClient.CompletedListener() { + @Override + public void completed(Bundle result) { + getTokenCompleted(request, result); + } + }; + + getTokenClient.setCompletedListener(callback); + return true; + } + + void getTokenCompleted(AuthorizationRequest request, Bundle result) { + getTokenClient = null; + + notifyBackgroundProcessingStop(); + + if (result != null) { + ArrayList currentPermissions = result.getStringArrayList(NativeProtocol.EXTRA_PERMISSIONS); + List permissions = request.getPermissions(); + if ((currentPermissions != null) && + ((permissions == null) || currentPermissions.containsAll(permissions))) { + // We got all the permissions we needed, so we can complete the auth now. + AccessToken token = AccessToken + .createFromNativeLogin(result, AccessTokenSource.FACEBOOK_APPLICATION_SERVICE); + Result outcome = Result.createTokenResult(pendingRequest, token); + completeAndValidate(outcome); + return; + } + + // We didn't get all the permissions we wanted, so update the request with just the permissions + // we still need. + List newPermissions = new ArrayList(); + for (String permission : permissions) { + if (!currentPermissions.contains(permission)) { + newPermissions.add(permission); + } + } + if (!newPermissions.isEmpty()) { + addLoggingExtra(EVENT_EXTRAS_NEW_PERMISSIONS, TextUtils.join(",", newPermissions)); + } + + request.setPermissions(newPermissions); + } + + tryNextHandler(); + } + } + + abstract class KatanaAuthHandler extends AuthHandler { + private static final long serialVersionUID = 1L; + + protected boolean tryIntent(Intent intent, int requestCode) { + if (intent == null) { + return false; + } + + try { + getStartActivityDelegate().startActivityForResult(intent, requestCode); + } catch (ActivityNotFoundException e) { + // We don't expect this to happen, since we've already validated the intent and bailed out before + // now if it couldn't be resolved. + return false; + } + + return true; + } + } + + class KatanaProxyAuthHandler extends KatanaAuthHandler { + private static final long serialVersionUID = 1L; + private String applicationId; + + @Override + String getNameForLogging() { + return "katana_proxy_auth"; + } + + @Override + boolean tryAuthorize(AuthorizationRequest request) { + applicationId = request.getApplicationId(); + + String e2e = getE2E(); + Intent intent = NativeProtocol.createProxyAuthIntent(context, request.getApplicationId(), + request.getPermissions(), e2e, request.isRerequest()); + + addLoggingExtra(ServerProtocol.DIALOG_PARAM_E2E, e2e); + + return tryIntent(intent, request.getRequestCode()); + } + + @Override + boolean onActivityResult(int requestCode, int resultCode, Intent data) { + // Handle stuff + Result outcome; + + if (data == null) { + // This happens if the user presses 'Back'. + outcome = Result.createCancelResult(pendingRequest, "Operation canceled"); + } else if (resultCode == Activity.RESULT_CANCELED) { + outcome = Result.createCancelResult(pendingRequest, data.getStringExtra("error")); + } else if (resultCode != Activity.RESULT_OK) { + outcome = Result.createErrorResult(pendingRequest, "Unexpected resultCode from authorization.", null); + } else { + outcome = handleResultOk(data); + } + + if (outcome != null) { + completeAndValidate(outcome); + } else { + tryNextHandler(); + } + return true; + } + + private Result handleResultOk(Intent data) { + Bundle extras = data.getExtras(); + String error = extras.getString("error"); + if (error == null) { + error = extras.getString("error_type"); + } + String errorCode = extras.getString("error_code"); + String errorMessage = extras.getString("error_message"); + if (errorMessage == null) { + errorMessage = extras.getString("error_description"); + } + + String e2e = extras.getString(NativeProtocol.FACEBOOK_PROXY_AUTH_E2E_KEY); + if (!Utility.isNullOrEmpty(e2e)) { + logWebLoginCompleted(applicationId, e2e); + } + + if (error == null && errorCode == null && errorMessage == null) { + AccessToken token = AccessToken.createFromWebBundle(pendingRequest.getPermissions(), extras, + AccessTokenSource.FACEBOOK_APPLICATION_WEB); + return Result.createTokenResult(pendingRequest, token); + } else if (ServerProtocol.errorsProxyAuthDisabled.contains(error)) { + return null; + } else if (ServerProtocol.errorsUserCanceled.contains(error)) { + return Result.createCancelResult(pendingRequest, null); + } else { + return Result.createErrorResult(pendingRequest, error, errorMessage, errorCode); + } + } + } + + private static String getE2E() { + JSONObject e2e = new JSONObject(); + try { + e2e.put("init", System.currentTimeMillis()); + } catch (JSONException e) { + } + return e2e.toString(); + } + + private void logWebLoginCompleted(String applicationId, String e2e) { + AppEventsLogger appEventsLogger = AppEventsLogger.newLogger(context, applicationId); + + Bundle parameters = new Bundle(); + parameters.putString(AnalyticsEvents.PARAMETER_WEB_LOGIN_E2E, e2e); + parameters.putLong(AnalyticsEvents.PARAMETER_WEB_LOGIN_SWITCHBACK_TIME, System.currentTimeMillis()); + parameters.putString(AnalyticsEvents.PARAMETER_APP_ID, applicationId); + + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_WEB_LOGIN_COMPLETE, null, parameters); + } + + static class AuthDialogBuilder extends WebDialog.Builder { + private static final String OAUTH_DIALOG = "oauth"; + static final String REDIRECT_URI = "fbconnect://success"; + private String e2e; + private boolean isRerequest; + + public AuthDialogBuilder(Context context, String applicationId, Bundle parameters) { + super(context, applicationId, OAUTH_DIALOG, parameters); + } + + public AuthDialogBuilder setE2E(String e2e) { + this.e2e = e2e; + return this; + } + + public AuthDialogBuilder setIsRerequest(boolean isRerequest) { + this.isRerequest = isRerequest; + return this; + } + + @Override + public WebDialog build() { + Bundle parameters = getParameters(); + parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, REDIRECT_URI); + parameters.putString(ServerProtocol.DIALOG_PARAM_CLIENT_ID, getApplicationId()); + parameters.putString(ServerProtocol.DIALOG_PARAM_E2E, e2e); + parameters.putString(ServerProtocol.DIALOG_PARAM_RESPONSE_TYPE, ServerProtocol.DIALOG_RESPONSE_TYPE_TOKEN); + parameters.putString(ServerProtocol.DIALOG_PARAM_RETURN_SCOPES, ServerProtocol.DIALOG_RETURN_SCOPES_TRUE); + + // Only set the rerequest auth type for non legacy requests + if (isRerequest && !Settings.getPlatformCompatibilityEnabled()) { + parameters.putString(ServerProtocol.DIALOG_PARAM_AUTH_TYPE, ServerProtocol.DIALOG_REREQUEST_AUTH_TYPE); + } + + return new WebDialog(getContext(), OAUTH_DIALOG, parameters, getTheme(), getListener()); + } + } + + static class AuthorizationRequest implements Serializable { + private static final long serialVersionUID = 1L; + + private transient final StartActivityDelegate startActivityDelegate; + private final SessionLoginBehavior loginBehavior; + private final int requestCode; + private boolean isLegacy = false; + private List permissions; + private final SessionDefaultAudience defaultAudience; + private final String applicationId; + private final String previousAccessToken; + private final String authId; + private boolean isRerequest = false; + + AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode, boolean isLegacy, + List permissions, SessionDefaultAudience defaultAudience, String applicationId, + String validateSameFbidAsToken, StartActivityDelegate startActivityDelegate, String authId) { + this.loginBehavior = loginBehavior; + this.requestCode = requestCode; + this.isLegacy = isLegacy; + this.permissions = permissions; + this.defaultAudience = defaultAudience; + this.applicationId = applicationId; + this.previousAccessToken = validateSameFbidAsToken; + this.startActivityDelegate = startActivityDelegate; + this.authId = authId; + } + + StartActivityDelegate getStartActivityDelegate() { + return startActivityDelegate; + } + + List getPermissions() { + return permissions; + } + + void setPermissions(List permissions) { + this.permissions = permissions; + } + + SessionLoginBehavior getLoginBehavior() { + return loginBehavior; + } + + int getRequestCode() { + return requestCode; + } + + SessionDefaultAudience getDefaultAudience() { + return defaultAudience; + } + + String getApplicationId() { + return applicationId; + } + + boolean isLegacy() { + return isLegacy; + } + + void setIsLegacy(boolean isLegacy) { + this.isLegacy = isLegacy; + } + + String getPreviousAccessToken() { + return previousAccessToken; + } + + boolean needsNewTokenValidation() { + return previousAccessToken != null && !isLegacy; + } + + String getAuthId() { + return authId; + } + + boolean isRerequest() { + return isRerequest; + } + + void setRerequest(boolean isRerequest) { + this.isRerequest = isRerequest; + } + } + + + static class Result implements Serializable { + private static final long serialVersionUID = 1L; + + enum Code { + SUCCESS("success"), + CANCEL("cancel"), + ERROR("error"); + + private final String loggingValue; + + Code(String loggingValue) { + this.loggingValue = loggingValue; + } + + // For consistency across platforms, we want to use specific string values when logging these results. + String getLoggingValue() { + return loggingValue; + } + } + + final Code code; + final AccessToken token; + final String errorMessage; + final String errorCode; + final AuthorizationRequest request; + Map loggingExtras; + + private Result(AuthorizationRequest request, Code code, AccessToken token, String errorMessage, + String errorCode) { + this.request = request; + this.token = token; + this.errorMessage = errorMessage; + this.code = code; + this.errorCode = errorCode; + } + + static Result createTokenResult(AuthorizationRequest request, AccessToken token) { + return new Result(request, Code.SUCCESS, token, null, null); + } + + static Result createCancelResult(AuthorizationRequest request, String message) { + return new Result(request, Code.CANCEL, null, message, null); + } + + static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription) { + return createErrorResult(request, errorType, errorDescription, null); + } + + static Result createErrorResult(AuthorizationRequest request, String errorType, String errorDescription, + String errorCode) { + String message = TextUtils.join(": ", Utility.asListNoNulls(errorType, errorDescription)); + return new Result(request, Code.ERROR, null, message, errorCode); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookAppLinkResolver.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookAppLinkResolver.java new file mode 100644 index 0000000000..e05e05f998 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookAppLinkResolver.java @@ -0,0 +1,234 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.net.Uri; +import android.os.Bundle; +import bolts.AppLink; +import bolts.AppLinkResolver; +import bolts.Continuation; +import bolts.Task; +import com.facebook.model.GraphObject; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.*; + +/** + * Provides an implementation for the {@link AppLinkResolver AppLinkResolver} interface that uses the Facebook App Link + * index to solve App Links, given a Url. It also provides an additional helper method that can resolve multiple App + * Links in a single call. + */ +public class FacebookAppLinkResolver implements AppLinkResolver { + + private static final String APP_LINK_ANDROID_TARGET_KEY = "android"; + private static final String APP_LINK_WEB_TARGET_KEY = "web"; + private static final String APP_LINK_TARGET_PACKAGE_KEY = "package"; + private static final String APP_LINK_TARGET_CLASS_KEY = "class"; + private static final String APP_LINK_TARGET_APP_NAME_KEY = "app_name"; + private static final String APP_LINK_TARGET_URL_KEY = "url"; + private static final String APP_LINK_TARGET_SHOULD_FALLBACK_KEY = "should_fallback"; + + private final HashMap cachedAppLinks = new HashMap(); + + /** + * Asynchronously resolves App Link data for the passed in Uri + * + * @param uri Uri to be resolved into an App Link + * @return A Task that, when successful, will return an AppLink for the passed in Uri. This may be null if no App + * Link data was found for this Uri. + * In the case of general server errors, the task will be completed with the corresponding error. + */ + public Task getAppLinkFromUrlInBackground(final Uri uri) { + ArrayList uris = new ArrayList(); + uris.add(uri); + + Task> resolveTask = getAppLinkFromUrlsInBackground(uris); + + return resolveTask.onSuccess(new Continuation, AppLink>() { + @Override + public AppLink then(Task> resolveUrisTask) throws Exception { + return resolveUrisTask.getResult().get(uri); + } + }); + } + + /** + * Asynchronously resolves App Link data for multiple Urls + * + * @param uris A list of Uri objects to resolve into App Links + * @return A Task that, when successful, will return a Map of Uri->AppLink for each Uri that was successfully + * resolved into an App Link. Uris that could not be resolved into App Links will not be present in the Map. + * In the case of general server errors, the task will be completed with the corresponding error. + */ + public Task> getAppLinkFromUrlsInBackground(List uris) { + final Map appLinkResults = new HashMap(); + final HashSet urisToRequest = new HashSet(); + StringBuilder graphRequestFields = new StringBuilder(); + + for (Uri uri : uris) { + AppLink appLink = null; + synchronized (cachedAppLinks) { + appLink = cachedAppLinks.get(uri); + } + + if (appLink != null) { + appLinkResults.put(uri, appLink); + } else { + if (!urisToRequest.isEmpty()) { + graphRequestFields.append(','); + } + graphRequestFields.append(uri.toString()); + urisToRequest.add(uri); + } + } + + if (urisToRequest.isEmpty()) { + return Task.forResult(appLinkResults); + } + + final Task>.TaskCompletionSource taskCompletionSource = Task.create(); + + Bundle appLinkRequestParameters = new Bundle(); + appLinkRequestParameters.putString("type", "al"); + appLinkRequestParameters.putString("ids", graphRequestFields.toString()); + appLinkRequestParameters.putString( + "fields", + String.format("%s,%s", APP_LINK_ANDROID_TARGET_KEY, APP_LINK_WEB_TARGET_KEY)); + + Request appLinkRequest = new Request( + null, /* Session */ + "", /* Graph path */ + appLinkRequestParameters, /* Query parameters */ + null, /* HttpMethod */ + new Request.Callback() { /* Callback */ + @Override + public void onCompleted(Response response) { + FacebookRequestError error = response.getError(); + if (error != null) { + taskCompletionSource.setError(error.getException()); + return; + } + + GraphObject responseObject = response.getGraphObject(); + JSONObject responseJson = responseObject != null ? responseObject.getInnerJSONObject() : null; + if (responseJson == null) { + taskCompletionSource.setResult(appLinkResults); + return; + } + + for (Uri uri : urisToRequest) { + String uriString = uri.toString(); + if (!responseJson.has(uriString)) { + continue; + } + + JSONObject urlData = null; + try { + urlData = responseJson.getJSONObject(uri.toString()); + JSONArray rawTargets = urlData.getJSONArray(APP_LINK_ANDROID_TARGET_KEY); + + int targetsCount = rawTargets.length(); + List targets = new ArrayList(targetsCount); + + for (int i = 0; i < targetsCount; i++) { + AppLink.Target target = getAndroidTargetFromJson(rawTargets.getJSONObject(i)); + if (target != null) { + targets.add(target); + } + } + + Uri webFallbackUrl = getWebFallbackUriFromJson(uri, urlData); + AppLink appLink = new AppLink(uri, targets, webFallbackUrl); + + appLinkResults.put(uri, appLink); + synchronized (cachedAppLinks) { + cachedAppLinks.put(uri, appLink); + } + } catch (JSONException e) { + // The data for this uri was missing or badly formed. + continue; + } + } + + taskCompletionSource.setResult(appLinkResults); + } + }); + + appLinkRequest.executeAsync(); + + return taskCompletionSource.getTask(); + } + + private static AppLink.Target getAndroidTargetFromJson(JSONObject targetJson) { + String packageName = tryGetStringFromJson(targetJson, APP_LINK_TARGET_PACKAGE_KEY, null); + if (packageName == null) { + // Package name is mandatory for each Android target + return null; + } + String className = tryGetStringFromJson(targetJson, APP_LINK_TARGET_CLASS_KEY, null); + String appName = tryGetStringFromJson(targetJson, APP_LINK_TARGET_APP_NAME_KEY, null); + String targetUrlString = tryGetStringFromJson(targetJson, APP_LINK_TARGET_URL_KEY, null); + Uri targetUri = null; + if (targetUrlString != null) { + targetUri = Uri.parse(targetUrlString); + } + + return new AppLink.Target(packageName, className, targetUri, appName); + } + + private static Uri getWebFallbackUriFromJson(Uri sourceUrl, JSONObject urlData) { + // Try and get a web target. This is best effort. Any failures results in null being returned. + try { + JSONObject webTarget = urlData.getJSONObject(APP_LINK_WEB_TARGET_KEY); + boolean shouldFallback = tryGetBooleanFromJson(webTarget, APP_LINK_TARGET_SHOULD_FALLBACK_KEY, true); + if (!shouldFallback) { + // Don't use a fallback url + return null; + } + + String webTargetUrlString = tryGetStringFromJson(webTarget, APP_LINK_TARGET_URL_KEY, null); + Uri webUri = null; + if (webTargetUrlString != null) { + webUri = Uri.parse(webTargetUrlString); + } + + // If we weren't able to parse a url from the web target, use the source url + return webUri != null ? webUri: sourceUrl; + } catch (JSONException e) { + // If we were missing a web target, just use the source as the web url + return sourceUrl; + } + } + + private static String tryGetStringFromJson(JSONObject json, String propertyName, String defaultValue) { + try { + return json.getString(propertyName); + } catch(JSONException e) { + return defaultValue; + } + } + + private static boolean tryGetBooleanFromJson(JSONObject json, String propertyName, boolean defaultValue) { + try { + return json.getBoolean(propertyName); + } catch (JSONException e) { + return defaultValue; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookAuthorizationException.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookAuthorizationException.java new file mode 100644 index 0000000000..59ef9742a3 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookAuthorizationException.java @@ -0,0 +1,65 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * An Exception indicating that a Session failed to open or obtain new permissions. + */ +public class FacebookAuthorizationException extends FacebookException { + static final long serialVersionUID = 1; + + /** + * Constructs a FacebookAuthorizationException with no additional + * information. + */ + public FacebookAuthorizationException() { + super(); + } + + /** + * Constructs a FacebookAuthorizationException with a message. + * + * @param message + * A String to be returned from getMessage. + */ + public FacebookAuthorizationException(String message) { + super(message); + } + + /** + * Constructs a FacebookAuthorizationException with a message and inner + * error. + * + * @param message + * A String to be returned from getMessage. + * @param throwable + * A Throwable to be returned from getCause. + */ + public FacebookAuthorizationException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Constructs a FacebookAuthorizationException with an inner error. + * + * @param throwable + * A Throwable to be returned from getCause. + */ + public FacebookAuthorizationException(Throwable throwable) { + super(throwable); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookBroadcastReceiver.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookBroadcastReceiver.java new file mode 100644 index 0000000000..1704cdaf80 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookBroadcastReceiver.java @@ -0,0 +1,41 @@ +package com.facebook; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import com.facebook.internal.NativeProtocol; + +/** + * This class implements a simple BroadcastReceiver designed to listen for broadcast notifications from the + * Facebook app. At present, these notifications consistent of success/failure notifications for photo upload + * operations that happen in the background. + * + * Applications may subclass this class and register it in their AndroidManifest.xml, listening on the + * com.facebook.platform.AppCallResultBroadcast action. + */ +public class FacebookBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String appCallId = intent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID); + String action = intent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION); + if (appCallId != null && action != null) { + Bundle extras = intent.getExtras(); + + if (NativeProtocol.isErrorResult(intent)) { + onFailedAppCall(appCallId, action, extras); + } else { + onSuccessfulAppCall(appCallId, action, extras); + } + } + } + + protected void onSuccessfulAppCall(String appCallId, String action, Bundle extras) { + // Default does nothing. + } + + protected void onFailedAppCall(String appCallId, String action, Bundle extras) { + // Default does nothing. + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookDialogException.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookDialogException.java new file mode 100644 index 0000000000..f59b99c8f6 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookDialogException.java @@ -0,0 +1,53 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Represents an error condition relating to displaying a Facebook Web dialog. + */ +public class FacebookDialogException extends FacebookException { + static final long serialVersionUID = 1; + private int errorCode; + private String failingUrl; + + /** + * Constructs a new FacebookException. + */ + public FacebookDialogException(String message, int errorCode, String failingUrl) { + super(message); + this.errorCode = errorCode; + this.failingUrl = failingUrl; + } + + /** + * Gets the error code received by the WebView. See: + * http://developer.android.com/reference/android/webkit/WebViewClient.html + * + * @return the error code + */ + public int getErrorCode() { + return errorCode; + } + + /** + * Gets the URL that the dialog was trying to load. + * @return the URL + */ + public String getFailingUrl() { + return failingUrl; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookException.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookException.java new file mode 100644 index 0000000000..4caeda7f76 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookException.java @@ -0,0 +1,63 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Represents an error condition specific to the Facebook SDK for Android. + */ +public class FacebookException extends RuntimeException { + static final long serialVersionUID = 1; + + /** + * Constructs a new FacebookException. + */ + public FacebookException() { + super(); + } + + /** + * Constructs a new FacebookException. + * + * @param message + * the detail message of this exception + */ + public FacebookException(String message) { + super(message); + } + + /** + * Constructs a new FacebookException. + * + * @param message + * the detail message of this exception + * @param throwable + * the cause of this exception + */ + public FacebookException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Constructs a new FacebookException. + * + * @param throwable + * the cause of this exception + */ + public FacebookException(Throwable throwable) { + super(throwable); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookGraphObjectException.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookGraphObjectException.java new file mode 100644 index 0000000000..b513706de9 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookGraphObjectException.java @@ -0,0 +1,63 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Represents an error specific to the {@link com.facebook.model.GraphObject GraphObject} class. + */ +public class FacebookGraphObjectException extends FacebookException { + static final long serialVersionUID = 1; + + /** + * Constructs a new FacebookGraphObjectException. + */ + public FacebookGraphObjectException() { + super(); + } + + /** + * Constructs a new FacebookGraphObjectException. + * + * @param message + * the detail message of this exception + */ + public FacebookGraphObjectException(String message) { + super(message); + } + + /** + * Constructs a new FacebookGraphObjectException. + * + * @param message + * the detail message of this exception + * @param throwable + * the cause of this exception + */ + public FacebookGraphObjectException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Constructs a new FacebookGraphObjectException. + * + * @param throwable + * the cause of this exception + */ + public FacebookGraphObjectException(Throwable throwable) { + super(throwable); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookOperationCanceledException.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookOperationCanceledException.java new file mode 100644 index 0000000000..9fcf11dd47 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookOperationCanceledException.java @@ -0,0 +1,59 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * An Exception indicating that an operation was canceled before it completed. + */ +public class FacebookOperationCanceledException extends FacebookException { + static final long serialVersionUID = 1; + + /** + * Constructs a FacebookOperationCanceledException with no additional information. + */ + public FacebookOperationCanceledException() { + super(); + } + + /** + * Constructs a FacebookOperationCanceledException with a message. + * + * @param message A String to be returned from getMessage. + */ + public FacebookOperationCanceledException(String message) { + super(message); + } + + /** + * Constructs a FacebookOperationCanceledException with a message and inner error. + * + * @param message A String to be returned from getMessage. + * @param throwable A Throwable to be returned from getCause. + */ + public FacebookOperationCanceledException(String message, Throwable throwable) { + super(message, throwable); + } + + /** + * Constructs a FacebookOperationCanceledException with an inner error. + * + * @param throwable A Throwable to be returned from getCause. + */ + public FacebookOperationCanceledException(Throwable throwable) { + super(throwable); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookRequestError.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookRequestError.java new file mode 100644 index 0000000000..937e2b5805 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookRequestError.java @@ -0,0 +1,453 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import com.facebook.android.R; +import com.facebook.internal.Utility; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; + +/** + * This class represents an error that occurred during a Facebook request. + *

+ * In general, one would call {@link #getCategory()} to determine the type + * of error that occurred, and act accordingly. The app can also call + * {@link #getUserActionMessageId()} in order to get the resource id for a + * string that can be displayed to the user. For more information on error + * handling, see + * https://developers.facebook.com/docs/reference/api/errors/ + */ +public final class FacebookRequestError { + + /** Represents an invalid or unknown error code from the server. */ + public static final int INVALID_ERROR_CODE = -1; + + /** + * Indicates that there was no valid HTTP status code returned, indicating + * that either the error occurred locally, before the request was sent, or + * that something went wrong with the HTTP connection. Check the exception + * from {@link #getException()}; + */ + public static final int INVALID_HTTP_STATUS_CODE = -1; + + private static final int INVALID_MESSAGE_ID = 0; + + private static final String CODE_KEY = "code"; + private static final String BODY_KEY = "body"; + private static final String ERROR_KEY = "error"; + private static final String ERROR_TYPE_FIELD_KEY = "type"; + private static final String ERROR_CODE_FIELD_KEY = "code"; + private static final String ERROR_MESSAGE_FIELD_KEY = "message"; + private static final String ERROR_CODE_KEY = "error_code"; + private static final String ERROR_SUB_CODE_KEY = "error_subcode"; + private static final String ERROR_MSG_KEY = "error_msg"; + private static final String ERROR_REASON_KEY = "error_reason"; + + private static class Range { + private final int start, end; + + private Range(int start, int end) { + this.start = start; + this.end = end; + } + + boolean contains(int value) { + return start <= value && value <= end; + } + } + + private static final int EC_UNKNOWN_ERROR = 1; + private static final int EC_SERVICE_UNAVAILABLE = 2; + private static final int EC_APP_TOO_MANY_CALLS = 4; + private static final int EC_USER_TOO_MANY_CALLS = 17; + private static final int EC_PERMISSION_DENIED = 10; + private static final int EC_INVALID_SESSION = 102; + private static final int EC_INVALID_TOKEN = 190; + private static final Range EC_RANGE_PERMISSION = new Range(200, 299); + private static final int EC_APP_NOT_INSTALLED = 458; + private static final int EC_USER_CHECKPOINTED = 459; + private static final int EC_PASSWORD_CHANGED = 460; + private static final int EC_EXPIRED = 463; + private static final int EC_UNCONFIRMED_USER = 464; + + private static final Range HTTP_RANGE_SUCCESS = new Range(200, 299); + private static final Range HTTP_RANGE_CLIENT_ERROR = new Range(400, 499); + private static final Range HTTP_RANGE_SERVER_ERROR = new Range(500, 599); + + private final int userActionMessageId; + private final boolean shouldNotifyUser; + private final Category category; + private final int requestStatusCode; + private final int errorCode; + private final int subErrorCode; + private final String errorType; + private final String errorMessage; + private final JSONObject requestResult; + private final JSONObject requestResultBody; + private final Object batchRequestResult; + private final HttpURLConnection connection; + private final FacebookException exception; + + private FacebookRequestError(int requestStatusCode, int errorCode, + int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody, + JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection, + FacebookException exception) { + this.requestStatusCode = requestStatusCode; + this.errorCode = errorCode; + this.subErrorCode = subErrorCode; + this.errorType = errorType; + this.errorMessage = errorMessage; + this.requestResultBody = requestResultBody; + this.requestResult = requestResult; + this.batchRequestResult = batchRequestResult; + this.connection = connection; + + boolean isLocalException = false; + if (exception != null) { + this.exception = exception; + isLocalException = true; + } else { + this.exception = new FacebookServiceException(this, errorMessage); + } + + // Initializes the error categories based on the documented error codes as outlined here + // https://developers.facebook.com/docs/reference/api/errors/ + Category errorCategory = null; + int messageId = INVALID_MESSAGE_ID; + boolean shouldNotify = false; + if (isLocalException) { + errorCategory = Category.CLIENT; + messageId = INVALID_MESSAGE_ID; + } else { + if (errorCode == EC_UNKNOWN_ERROR || errorCode == EC_SERVICE_UNAVAILABLE) { + errorCategory = Category.SERVER; + } else if (errorCode == EC_APP_TOO_MANY_CALLS || errorCode == EC_USER_TOO_MANY_CALLS) { + errorCategory = Category.THROTTLING; + } else if (errorCode == EC_PERMISSION_DENIED || EC_RANGE_PERMISSION.contains(errorCode)) { + errorCategory = Category.PERMISSION; + messageId = R.string.com_facebook_requesterror_permissions; + } else if (errorCode == EC_INVALID_SESSION || errorCode == EC_INVALID_TOKEN) { + if (subErrorCode == EC_USER_CHECKPOINTED || subErrorCode == EC_UNCONFIRMED_USER) { + errorCategory = Category.AUTHENTICATION_RETRY; + messageId = R.string.com_facebook_requesterror_web_login; + shouldNotify = true; + } else { + errorCategory = Category.AUTHENTICATION_REOPEN_SESSION; + + if ((subErrorCode == EC_APP_NOT_INSTALLED) || (subErrorCode == EC_EXPIRED)) { + messageId = R.string.com_facebook_requesterror_relogin; + } else if (subErrorCode == EC_PASSWORD_CHANGED) { + messageId = R.string.com_facebook_requesterror_password_changed; + } else { + messageId = R.string.com_facebook_requesterror_reconnect; + shouldNotify = true; + } + } + } + + if (errorCategory == null) { + if (HTTP_RANGE_CLIENT_ERROR.contains(requestStatusCode)) { + errorCategory = Category.BAD_REQUEST; + } else if (HTTP_RANGE_SERVER_ERROR.contains(requestStatusCode)) { + errorCategory = Category.SERVER; + } else { + errorCategory = Category.OTHER; + } + } + } + + this.category = errorCategory; + this.userActionMessageId = messageId; + this.shouldNotifyUser = shouldNotify; + } + + private FacebookRequestError(int requestStatusCode, int errorCode, + int subErrorCode, String errorType, String errorMessage, JSONObject requestResultBody, + JSONObject requestResult, Object batchRequestResult, HttpURLConnection connection) { + this(requestStatusCode, errorCode, subErrorCode, errorType, errorMessage, + requestResultBody, requestResult, batchRequestResult, connection, null); + } + + FacebookRequestError(HttpURLConnection connection, Exception exception) { + this(INVALID_HTTP_STATUS_CODE, INVALID_ERROR_CODE, INVALID_ERROR_CODE, + null, null, null, null, null, connection, + (exception instanceof FacebookException) ? + (FacebookException) exception : new FacebookException(exception)); + } + + public FacebookRequestError(int errorCode, String errorType, String errorMessage) { + this(INVALID_HTTP_STATUS_CODE, errorCode, INVALID_ERROR_CODE, errorType, errorMessage, + null, null, null, null, null); + } + + /** + * Returns the resource id for a user-friendly message for the application to + * present to the user. + * + * @return a user-friendly message to present to the user + */ + public int getUserActionMessageId() { + return userActionMessageId; + } + + /** + * Returns whether direct user action is required to successfully continue with the Facebook + * operation. If user action is required, apps can also call {@link #getUserActionMessageId()} + * in order to get a resource id for a message to show the user. + * + * @return whether direct user action is required + */ + public boolean shouldNotifyUser() { + return shouldNotifyUser; + } + + /** + * Returns the category in which the error belongs. Applications can use the category + * to determine how best to handle the errors (e.g. exponential backoff for retries if + * being throttled). + * + * @return the category in which the error belong + */ + public Category getCategory() { + return category; + } + + /** + * Returns the HTTP status code for this particular request. + * + * @return the HTTP status code for the request + */ + public int getRequestStatusCode() { + return requestStatusCode; + } + + /** + * Returns the error code returned from Facebook. + * + * @return the error code returned from Facebook + */ + public int getErrorCode() { + return errorCode; + } + + /** + * Returns the sub-error code returned from Facebook. + * + * @return the sub-error code returned from Facebook + */ + public int getSubErrorCode() { + return subErrorCode; + } + + /** + * Returns the type of error as a raw string. This is generally less useful + * than using the {@link #getCategory()} method, but can provide further details + * on the error. + * + * @return the type of error as a raw string + */ + public String getErrorType() { + return errorType; + } + + /** + * Returns the error message returned from Facebook. + * + * @return the error message returned from Facebook + */ + public String getErrorMessage() { + if (errorMessage != null) { + return errorMessage; + } else { + return exception.getLocalizedMessage(); + } + } + + /** + * Returns the body portion of the response corresponding to the request from Facebook. + * + * @return the body of the response for the request + */ + public JSONObject getRequestResultBody() { + return requestResultBody; + } + + /** + * Returns the full JSON response for the corresponding request. In a non-batch request, + * this would be the raw response in the form of a JSON object. In a batch request, this + * result will contain the body of the response as well as the HTTP headers that pertain + * to the specific request (in the form of a "headers" JSONArray). + * + * @return the full JSON response for the request + */ + public JSONObject getRequestResult() { + return requestResult; + } + + /** + * Returns the full JSON response for the batch request. If the request was not a batch + * request, then the result from this method is the same as {@link #getRequestResult()}. + * In case of a batch request, the result will be a JSONArray where the elements + * correspond to the requests in the batch. Callers should check the return type against + * either JSONObject or JSONArray and cast accordingly. + * + * @return the full JSON response for the batch + */ + public Object getBatchRequestResult() { + return batchRequestResult; + } + + /** + * Returns the HTTP connection that was used to make the request. + * + * @return the HTTP connection used to make the request + */ + public HttpURLConnection getConnection() { + return connection; + } + + /** + * Returns the exception associated with this request, if any. + * + * @return the exception associated with this request + */ + public FacebookException getException() { + return exception; + } + + @Override + public String toString() { + return new StringBuilder("{HttpStatus: ") + .append(requestStatusCode) + .append(", errorCode: ") + .append(errorCode) + .append(", errorType: ") + .append(errorType) + .append(", errorMessage: ") + .append(getErrorMessage()) + .append("}") + .toString(); + } + + static FacebookRequestError checkResponseAndCreateError(JSONObject singleResult, + Object batchResult, HttpURLConnection connection) { + try { + if (singleResult.has(CODE_KEY)) { + int responseCode = singleResult.getInt(CODE_KEY); + Object body = Utility.getStringPropertyAsJSON(singleResult, BODY_KEY, + Response.NON_JSON_RESPONSE_PROPERTY); + + if (body != null && body instanceof JSONObject) { + JSONObject jsonBody = (JSONObject) body; + // Does this response represent an error from the service? We might get either an "error" + // with several sub-properties, or else one or more top-level fields containing error info. + String errorType = null; + String errorMessage = null; + int errorCode = INVALID_ERROR_CODE; + int errorSubCode = INVALID_ERROR_CODE; + + boolean hasError = false; + if (jsonBody.has(ERROR_KEY)) { + // We assume the error object is correctly formatted. + JSONObject error = (JSONObject) Utility.getStringPropertyAsJSON(jsonBody, ERROR_KEY, null); + + errorType = error.optString(ERROR_TYPE_FIELD_KEY, null); + errorMessage = error.optString(ERROR_MESSAGE_FIELD_KEY, null); + errorCode = error.optInt(ERROR_CODE_FIELD_KEY, INVALID_ERROR_CODE); + errorSubCode = error.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE); + hasError = true; + } else if (jsonBody.has(ERROR_CODE_KEY) || jsonBody.has(ERROR_MSG_KEY) + || jsonBody.has(ERROR_REASON_KEY)) { + errorType = jsonBody.optString(ERROR_REASON_KEY, null); + errorMessage = jsonBody.optString(ERROR_MSG_KEY, null); + errorCode = jsonBody.optInt(ERROR_CODE_KEY, INVALID_ERROR_CODE); + errorSubCode = jsonBody.optInt(ERROR_SUB_CODE_KEY, INVALID_ERROR_CODE); + hasError = true; + } + + if (hasError) { + return new FacebookRequestError(responseCode, errorCode, errorSubCode, + errorType, errorMessage, jsonBody, singleResult, batchResult, connection); + } + } + + // If we didn't get error details, but we did get a failure response code, report it. + if (!HTTP_RANGE_SUCCESS.contains(responseCode)) { + return new FacebookRequestError(responseCode, INVALID_ERROR_CODE, + INVALID_ERROR_CODE, null, null, + singleResult.has(BODY_KEY) ? + (JSONObject) Utility.getStringPropertyAsJSON( + singleResult, BODY_KEY, Response.NON_JSON_RESPONSE_PROPERTY) : null, + singleResult, batchResult, connection); + } + } + } catch (JSONException e) { + // defer the throwing of a JSONException to the graph object proxy + } + return null; + } + + /** + * An enum that represents the Facebook SDK classification for the error that occurred. + */ + public enum Category { + /** + * Indicates that the error is authentication related, and that the app should retry + * the request after some user action. + */ + AUTHENTICATION_RETRY, + + /** + * Indicates that the error is authentication related, and that the app should close + * the session and reopen it. + */ + AUTHENTICATION_REOPEN_SESSION, + + /** Indicates that the error is permission related. */ + PERMISSION, + + /** + * Indicates that the error implies the server had an unexpected failure or may be + * temporarily unavailable. + */ + SERVER, + + /** Indicates that the error results from the server throttling the client. */ + THROTTLING, + + /** + * Indicates that the error is Facebook-related but cannot be categorized at this time, + * and is likely newer than the current version of the SDK. + */ + OTHER, + + /** + * Indicates that the error is an application error resulting in a bad or malformed + * request to the server. + */ + BAD_REQUEST, + + /** + * Indicates that this is a client-side error. Examples of this can include, but are + * not limited to, JSON parsing errors or {@link java.io.IOException}s. + */ + CLIENT + }; + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookSdkVersion.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookSdkVersion.java new file mode 100644 index 0000000000..7525134b4c --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookSdkVersion.java @@ -0,0 +1,21 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +final class FacebookSdkVersion { + public static final String BUILD = "3.15.0"; +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookServiceException.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookServiceException.java new file mode 100644 index 0000000000..0ff4f84f8f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/FacebookServiceException.java @@ -0,0 +1,63 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Represents an error returned from the Facebook service in response to a request. + */ +public class FacebookServiceException extends FacebookException { + + private final FacebookRequestError error; + + private static final long serialVersionUID = 1; + + /** + * Constructs a new FacebookServiceException. + * + * @param error the error from the request + */ + public FacebookServiceException(FacebookRequestError error, String errorMessage) { + super(errorMessage); + this.error = error; + } + + /** + * Returns an object that encapsulates complete information representing the error returned by Facebook. + * + * @return complete information representing the error. + */ + public final FacebookRequestError getRequestError() { + return error; + } + + @Override + public final String toString() { + return new StringBuilder() + .append("{FacebookServiceException: ") + .append("httpResponseCode: ") + .append(error.getRequestStatusCode()) + .append(", facebookErrorCode: ") + .append(error.getErrorCode()) + .append(", facebookErrorType: ") + .append(error.getErrorType()) + .append(", message: ") + .append(error.getErrorMessage()) + .append("}") + .toString(); + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/GetTokenClient.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/GetTokenClient.java new file mode 100644 index 0000000000..23cb31bf8f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/GetTokenClient.java @@ -0,0 +1,36 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + + +import android.content.Context; +import android.os.*; +import com.facebook.internal.NativeProtocol; +import com.facebook.internal.PlatformServiceClient; + +final class GetTokenClient extends PlatformServiceClient { + + GetTokenClient(Context context, String applicationId) { + super(context, NativeProtocol.MESSAGE_GET_ACCESS_TOKEN_REQUEST, NativeProtocol.MESSAGE_GET_ACCESS_TOKEN_REPLY, + NativeProtocol.PROTOCOL_VERSION_20121101, applicationId); + } + + @Override + protected void populateRequestBundle(Bundle data) { + } +} + diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/HttpMethod.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/HttpMethod.java new file mode 100644 index 0000000000..bf355bd9d3 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/HttpMethod.java @@ -0,0 +1,37 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Enumeration of HTTP methods supported by Request + */ +public enum HttpMethod { + /** + * Use HTTP method "GET" for the request + */ + GET, + + /** + * Use HTTP method "POST" for the request + */ + POST, + + /** + * Use HTTP method "DELETE" for the request + */ + DELETE, +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/InsightsLogger.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/InsightsLogger.java new file mode 100644 index 0000000000..168125f733 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/InsightsLogger.java @@ -0,0 +1,94 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import android.os.Bundle; +import com.facebook.internal.Logger; + +import java.math.BigDecimal; +import java.util.Currency; + +/** + * This class is deprecated. Please use {@link AppEventsLogger} instead. + */ +@Deprecated +public class InsightsLogger { + private static final String EVENT_PARAMETER_PIXEL_ID = "fb_offsite_pixel_id"; + private static final String EVENT_PARAMETER_PIXEL_VALUE = "fb_offsite_pixel_value"; + + private static final String EVENT_NAME_LOG_CONVERSION_PIXEL = "fb_log_offsite_pixel"; + + private AppEventsLogger appEventsLogger; + + private InsightsLogger(Context context, String applicationId, Session session) { + appEventsLogger = AppEventsLogger.newLogger(context, applicationId, session); + } + + /** + * Deprecated. Please use {@link AppEventsLogger} instead. + */ + public static InsightsLogger newLogger(Context context, String clientToken) { + return new InsightsLogger(context, null, null); + } + + /** + * Deprecated. Please use {@link AppEventsLogger} instead. + */ + public static InsightsLogger newLogger(Context context, String clientToken, String applicationId) { + return new InsightsLogger(context, applicationId, null); + } + + /** + * Deprecated. Please use {@link AppEventsLogger} instead. + */ + public static InsightsLogger newLogger(Context context, String clientToken, String applicationId, Session session) { + return new InsightsLogger(context, applicationId, session); + } + + /** + * Deprecated. Please use {@link AppEventsLogger} instead. + */ + public void logPurchase(BigDecimal purchaseAmount, Currency currency) { + logPurchase(purchaseAmount, currency, null); + } + + /** + * Deprecated. Please use {@link AppEventsLogger} instead. + */ + public void logPurchase(BigDecimal purchaseAmount, Currency currency, Bundle parameters) { + appEventsLogger.logPurchase(purchaseAmount, currency, parameters); + } + + /** + * Deprecated. Please use {@link AppEventsLogger} instead. + */ + public void logConversionPixel(String pixelId, double valueOfPixel) { + + if (pixelId == null) { + Logger.log(LoggingBehavior.DEVELOPER_ERRORS, "Insights", "pixelID cannot be null"); + return; + } + + Bundle parameters = new Bundle(); + parameters.putString(EVENT_PARAMETER_PIXEL_ID, pixelId); + parameters.putDouble(EVENT_PARAMETER_PIXEL_VALUE, valueOfPixel); + + appEventsLogger.logEvent(EVENT_NAME_LOG_CONVERSION_PIXEL, valueOfPixel, parameters); + AppEventsLogger.eagerFlush(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/LegacyHelper.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/LegacyHelper.java new file mode 100644 index 0000000000..dcdfd8561f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/LegacyHelper.java @@ -0,0 +1,31 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Bundle; + +/** + * LegacyHelper is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the methods in this class is unsupported, and they may be modified or removed without warning at + * any time. + */ +public class LegacyHelper { + @Deprecated + public static void extendTokenCompleted(Session session, Bundle bundle) { + session.extendTokenCompleted(bundle); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/LoggingBehavior.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/LoggingBehavior.java new file mode 100644 index 0000000000..5fb198e9c5 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/LoggingBehavior.java @@ -0,0 +1,53 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Specifies different categories of logging messages that can be generated. + * + * @see Settings#addLoggingBehavior(LoggingBehavior) + */ +public enum LoggingBehavior { + /** + * Indicates that HTTP requests and a summary of responses should be logged. + */ + REQUESTS, + /** + * Indicates that access tokens should be logged as part of the request logging; normally they are not. + */ + INCLUDE_ACCESS_TOKENS, + /** + * Indicates that the entire raw HTTP response for each request should be logged. + */ + INCLUDE_RAW_RESPONSES, + /** + * Indicates that cache operations should be logged. + */ + CACHE, + /** + * Indicates the App Events-related operations should be logged. + */ + APP_EVENTS, + /** + * Indicates that likely developer errors should be logged. (This is set by default in LoggingBehavior.) + */ + DEVELOPER_ERRORS + ; + + @Deprecated + public static final LoggingBehavior INSIGHTS = APP_EVENTS; +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/LoginActivity.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/LoginActivity.java new file mode 100644 index 0000000000..096f00eae7 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/LoginActivity.java @@ -0,0 +1,146 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import com.facebook.android.R; + +/** + * This Activity is a necessary part of the overall Facebook login process + * but is not meant to be used directly. Add this activity to your + * AndroidManifest.xml to ensure proper handling of Facebook login. + *

+ * {@code
+ * 
+ * }
+ * 
+ * Do not start this activity directly. + */ +public class LoginActivity extends Activity { + static final String RESULT_KEY = "com.facebook.LoginActivity:Result"; + + private static final String TAG = LoginActivity.class.getName(); + private static final String NULL_CALLING_PKG_ERROR_MSG = + "Cannot call LoginActivity with a null calling package. " + + "This can occur if the launchMode of the caller is singleInstance."; + private static final String SAVED_CALLING_PKG_KEY = "callingPackage"; + private static final String SAVED_AUTH_CLIENT = "authorizationClient"; + private static final String EXTRA_REQUEST = "request"; + + private String callingPackage; + private AuthorizationClient authorizationClient; + private AuthorizationClient.AuthorizationRequest request; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.com_facebook_login_activity_layout); + + if (savedInstanceState != null) { + callingPackage = savedInstanceState.getString(SAVED_CALLING_PKG_KEY); + authorizationClient = (AuthorizationClient) savedInstanceState.getSerializable(SAVED_AUTH_CLIENT); + } else { + callingPackage = getCallingPackage(); + authorizationClient = new AuthorizationClient(); + request = (AuthorizationClient.AuthorizationRequest) getIntent().getSerializableExtra(EXTRA_REQUEST); + } + + authorizationClient.setContext(this); + authorizationClient.setOnCompletedListener(new AuthorizationClient.OnCompletedListener() { + @Override + public void onCompleted(AuthorizationClient.Result outcome) { + onAuthClientCompleted(outcome); + } + }); + authorizationClient.setBackgroundProcessingListener(new AuthorizationClient.BackgroundProcessingListener() { + @Override + public void onBackgroundProcessingStarted() { + findViewById(R.id.com_facebook_login_activity_progress_bar).setVisibility(View.VISIBLE); + } + + @Override + public void onBackgroundProcessingStopped() { + findViewById(R.id.com_facebook_login_activity_progress_bar).setVisibility(View.GONE); + } + }); + } + + private void onAuthClientCompleted(AuthorizationClient.Result outcome) { + request = null; + + int resultCode = (outcome.code == AuthorizationClient.Result.Code.CANCEL) ? + RESULT_CANCELED : RESULT_OK; + + Bundle bundle = new Bundle(); + bundle.putSerializable(RESULT_KEY, outcome); + + Intent resultIntent = new Intent(); + resultIntent.putExtras(bundle); + setResult(resultCode, resultIntent); + + finish(); + } + + @Override + public void onResume() { + super.onResume(); + + // If the calling package is null, this generally means that the callee was started + // with a launchMode of singleInstance. Unfortunately, Android does not allow a result + // to be set when the callee is a singleInstance, so we log an error and return. + if (callingPackage == null) { + Log.e(TAG, NULL_CALLING_PKG_ERROR_MSG); + finish(); + return; + } + + authorizationClient.startOrContinueAuth(request); + } + + @Override + public void onPause() { + super.onPause(); + + authorizationClient.cancelCurrentHandler(); + findViewById(R.id.com_facebook_login_activity_progress_bar).setVisibility(View.GONE); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(SAVED_CALLING_PKG_KEY, callingPackage); + outState.putSerializable(SAVED_AUTH_CLIENT, authorizationClient); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + authorizationClient.onActivityResult(requestCode, resultCode, data); + } + + static Bundle populateIntentExtras(AuthorizationClient.AuthorizationRequest request) { + Bundle extras = new Bundle(); + extras.putSerializable(EXTRA_REQUEST, request); + return extras; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/NativeAppCallAttachmentStore.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/NativeAppCallAttachmentStore.java new file mode 100644 index 0000000000..dc41086562 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/NativeAppCallAttachmentStore.java @@ -0,0 +1,225 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; + +import java.io.*; +import java.net.URLEncoder; +import java.util.*; + +/** + *

This class works in conjunction with {@link NativeAppCallContentProvider} to allow apps to attach binary + * attachments (e.g., images) to native dialogs launched via the {@link com.facebook.widget.FacebookDialog} + * class. It stores attachments in temporary files and allows the Facebook application to retrieve them via + * the content provider.

+ * + *

Callers are generally not expected to need to use this class directly; + * see {@link com.facebook.widget.FacebookDialog.OpenGraphActionDialogBuilder#setImageAttachmentsForObject(String, + * java.util.List) OpenGraphActionDialogBuilder.setImageAttachmentsForObject} for an example of a function + * that will accept attachments, attach them to the native dialog call, and add them to the content provider + * automatically.

+ **/ +public final class NativeAppCallAttachmentStore implements NativeAppCallContentProvider.AttachmentDataSource { + private static final String TAG = NativeAppCallAttachmentStore.class.getName(); + static final String ATTACHMENTS_DIR_NAME = "com.facebook.NativeAppCallAttachmentStore.files"; + private static File attachmentsDirectory; + + /** + * Adds a number of bitmap attachments associated with a native app call. The attachments will be + * served via {@link NativeAppCallContentProvider#openFile(android.net.Uri, String) openFile}. + * + * @param context the Context the call is being made from + * @param callId the unique ID of the call + * @param imageAttachments a Map of attachment names to Bitmaps; the attachment names will be part of + * the URI processed by openFile + * @throws java.io.IOException + */ + public void addAttachmentsForCall(Context context, UUID callId, Map imageAttachments) { + Validate.notNull(context, "context"); + Validate.notNull(callId, "callId"); + Validate.containsNoNulls(imageAttachments.values(), "imageAttachments"); + Validate.containsNoNullOrEmpty(imageAttachments.keySet(), "imageAttachments"); + + addAttachments(context, callId, imageAttachments, new ProcessAttachment() { + @Override + public void processAttachment(Bitmap attachment, File outputFile) throws IOException { + FileOutputStream outputStream = new FileOutputStream(outputFile); + try { + attachment.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + } finally { + Utility.closeQuietly(outputStream); + } + } + }); + } + + /** + * Adds a number of bitmap attachment files associated with a native app call. The attachments will be + * served via {@link NativeAppCallContentProvider#openFile(android.net.Uri, String) openFile}. + * + * @param context the Context the call is being made from + * @param callId the unique ID of the call + * @param imageAttachments a Map of attachment names to Files containing the bitmaps; the attachment names will be + * part of the URI processed by openFile + * @throws java.io.IOException + */ + public void addAttachmentFilesForCall(Context context, UUID callId, Map imageAttachmentFiles) { + Validate.notNull(context, "context"); + Validate.notNull(callId, "callId"); + Validate.containsNoNulls(imageAttachmentFiles.values(), "imageAttachmentFiles"); + Validate.containsNoNullOrEmpty(imageAttachmentFiles.keySet(), "imageAttachmentFiles"); + + addAttachments(context, callId, imageAttachmentFiles, new ProcessAttachment() { + @Override + public void processAttachment(File attachment, File outputFile) throws IOException { + FileOutputStream outputStream = new FileOutputStream(outputFile); + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(attachment); + + byte[] buffer = new byte[1024]; + int len; + while ((len = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, len); + } + } finally { + Utility.closeQuietly(outputStream); + Utility.closeQuietly(inputStream); + } + } + }); + } + + private void addAttachments(Context context, UUID callId, Map attachments, + ProcessAttachment processor) { + if (attachments.size() == 0) { + return; + } + + // If this is the first time we've been instantiated, clean up any existing attachments. + if (attachmentsDirectory == null) { + cleanupAllAttachments(context); + } + + ensureAttachmentsDirectoryExists(context); + + List filesToCleanup = new ArrayList(); + + try { + for (Map.Entry entry : attachments.entrySet()) { + String attachmentName = entry.getKey(); + T attachment = entry.getValue(); + + File file = getAttachmentFile(callId, attachmentName, true); + filesToCleanup.add(file); + + processor.processAttachment(attachment, file); + } + } catch (IOException exception) { + Log.e(TAG, "Got unexpected exception:" + exception); + for (File file : filesToCleanup) { + try { + file.delete(); + } catch (Exception e) { + // Always try to delete other files. + } + } + throw new FacebookException(exception); + } + + } + + interface ProcessAttachment { + void processAttachment(T attachment, File outputFile) throws IOException; + } + + /** + * Removes any temporary files associated with a particular native app call. + * + * @param context the Context the call is being made from + * @param callId the unique ID of the call + */ + public void cleanupAttachmentsForCall(Context context, UUID callId) { + File dir = getAttachmentsDirectoryForCall(callId, false); + Utility.deleteDirectory(dir); + } + + @Override + public File openAttachment(UUID callId, String attachmentName) throws FileNotFoundException { + if (Utility.isNullOrEmpty(attachmentName) || + callId == null) { + throw new FileNotFoundException(); + } + + try { + return getAttachmentFile(callId, attachmentName, false); + } catch (IOException e) { + // We don't try to create the file, so we shouldn't get any IOExceptions. But if we do, just + // act like the file wasn't found. + throw new FileNotFoundException(); + } + } + + synchronized static File getAttachmentsDirectory(Context context) { + if (attachmentsDirectory == null) { + attachmentsDirectory = new File(context.getCacheDir(), ATTACHMENTS_DIR_NAME); + } + return attachmentsDirectory; + } + + File ensureAttachmentsDirectoryExists(Context context) { + File dir = getAttachmentsDirectory(context); + dir.mkdirs(); + return dir; + } + + File getAttachmentsDirectoryForCall(UUID callId, boolean create) { + if (attachmentsDirectory == null) { + return null; + } + + File dir = new File(attachmentsDirectory, callId.toString()); + if (create && !dir.exists()) { + dir.mkdirs(); + } + return dir; + } + + File getAttachmentFile(UUID callId, String attachmentName, boolean createDirs) throws IOException { + File dir = getAttachmentsDirectoryForCall(callId, createDirs); + if (dir == null) { + return null; + } + + try { + return new File(dir, URLEncoder.encode(attachmentName, "UTF-8")); + } catch (UnsupportedEncodingException e) { + return null; + } + } + + void cleanupAllAttachments(Context context) { + // Attachments directory may or may not exist; we won't create it if not, since we are just going to delete it. + File dir = getAttachmentsDirectory(context); + Utility.deleteDirectory(dir); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/NativeAppCallContentProvider.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/NativeAppCallContentProvider.java new file mode 100644 index 0000000000..bde165f581 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/NativeAppCallContentProvider.java @@ -0,0 +1,139 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.util.Log; +import android.util.Pair; + +import java.io.*; +import java.util.*; + +/** + *

Implements a + * ContentProvider that can be used to provide binary attachments (e.g., images) to calls made + * via @link FacebookDialog}. The {@link NativeAppCallAttachmentStore} class provides methods to attach + * and clean up the attachments. + * + *

Note that this ContentProvider is only necessary if an application wishes to attach images, etc., that are + * stored in memory and do not have another way to be referenced by a content URI. For images obtained from, + * e.g., the Camera or Gallery, that already have a content URI associated with them, use of this class is not + * necessary.

+ * + *

If an application wishes to attach images that are stored in-memory within the application, this content + * provider must be listed in the application's AndroidManifest.xml, and it should be named according to the + * pattern "com.facebook.app.NativeAppCallContentProvider{FACEBOOK_APP_ID}". See the + * {@link NativeAppCallContentProvider#getAttachmentUrl(String) getContentProviderName} method.

+ */ +public class NativeAppCallContentProvider extends ContentProvider { + private static final String TAG = NativeAppCallContentProvider.class.getName(); + private static final String ATTACHMENT_URL_BASE = "content://com.facebook.app.NativeAppCallContentProvider"; + + private final AttachmentDataSource dataSource; + + public NativeAppCallContentProvider() { + this(new NativeAppCallAttachmentStore()); + } + + NativeAppCallContentProvider(AttachmentDataSource dataSource) { + this.dataSource = dataSource; + } + + interface AttachmentDataSource { + File openAttachment(UUID callId, String attachmentName) throws FileNotFoundException; + } + + /** + * Returns the name of the content provider formatted correctly for constructing URLs. + * @param applicationId the Facebook application ID of the application + * @return the String to use as the authority portion of a content URI. + */ + public static String getAttachmentUrl(String applicationId, UUID callId, String attachmentName) { + return String.format("%s%s/%s/%s", ATTACHMENT_URL_BASE, applicationId, callId.toString(), attachmentName); + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] strings, String s, String[] strings2, String s2) { + return null; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues contentValues) { + return null; + } + + @Override + public int delete(Uri uri, String s, String[] strings) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { + return 0; + } + + @Override + public android.os.ParcelFileDescriptor openFile(android.net.Uri uri, java.lang.String mode) + throws java.io.FileNotFoundException { + + Pair callIdAndAttachmentName = parseCallIdAndAttachmentName(uri); + if (callIdAndAttachmentName == null) { + throw new FileNotFoundException(); + } + + try { + File file = dataSource.openAttachment(callIdAndAttachmentName.first, callIdAndAttachmentName.second); + + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } catch (FileNotFoundException exception) { + Log.e(TAG, "Got unexpected exception:" + exception); + throw exception; + } + } + + Pair parseCallIdAndAttachmentName(Uri uri) { + try { + // We don't do explicit format checking here. Malformed URIs may generate NullPointerExceptions or + // array bounds exceptions, which we'll catch and return null. All of these will result in a + // FileNotFoundException being thrown in openFile. + String callIdAndAttachmentName = uri.getPath().substring(1); + String [] parts = callIdAndAttachmentName.split("/"); + + String callIdString = parts[0]; + String attachmentName = parts[1]; + UUID callId = UUID.fromString(callIdString); + + return new Pair(callId, attachmentName); + } catch (Exception exception) { + return null; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/NonCachingTokenCachingStrategy.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/NonCachingTokenCachingStrategy.java new file mode 100644 index 0000000000..1f4c562163 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/NonCachingTokenCachingStrategy.java @@ -0,0 +1,39 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Bundle; + +/** + * Implements a trivial {@link TokenCachingStrategy} that does not actually cache any tokens. + * It is intended for use when an access token may be used on a temporary basis but should not be + * cached for future use (for instance, when handling a deep link). + */ +public class NonCachingTokenCachingStrategy extends TokenCachingStrategy { + @Override + public Bundle load() { + return null; + } + + @Override + public void save(Bundle bundle) { + } + + @Override + public void clear() { + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/ProgressNoopOutputStream.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/ProgressNoopOutputStream.java new file mode 100644 index 0000000000..e0e3770df8 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/ProgressNoopOutputStream.java @@ -0,0 +1,74 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Handler; + +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +class ProgressNoopOutputStream extends OutputStream implements RequestOutputStream { + private final Map progressMap = new HashMap(); + private final Handler callbackHandler; + + private Request currentRequest; + private RequestProgress currentRequestProgress; + private int batchMax; + + ProgressNoopOutputStream(Handler callbackHandler) { + this.callbackHandler = callbackHandler; + } + + public void setCurrentRequest(Request currentRequest) { + this.currentRequest = currentRequest; + this.currentRequestProgress = currentRequest != null? progressMap.get(currentRequest) : null; + } + + int getMaxProgress() { + return batchMax; + } + + Map getProgressMap() { + return progressMap; + } + + void addProgress(long size) { + if (currentRequestProgress == null) { + currentRequestProgress = new RequestProgress(callbackHandler, currentRequest); + progressMap.put(currentRequest, currentRequestProgress); + } + + currentRequestProgress.addToMax(size); + batchMax += size; + } + + @Override + public void write(byte[] buffer) { + addProgress(buffer.length); + } + + @Override + public void write(byte[] buffer, int offset, int length) { + addProgress(length); + } + + @Override + public void write(int oneByte) { + addProgress(1); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/ProgressOutputStream.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/ProgressOutputStream.java new file mode 100644 index 0000000000..0470e70283 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/ProgressOutputStream.java @@ -0,0 +1,120 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Handler; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +class ProgressOutputStream extends FilterOutputStream implements RequestOutputStream { + private final Map progressMap; + private final RequestBatch requests; + private final long threshold; + + private long batchProgress, lastReportedProgress, maxProgress; + private RequestProgress currentRequestProgress; + + ProgressOutputStream(OutputStream out, RequestBatch requests, Map progressMap, long maxProgress) { + super(out); + this.requests = requests; + this.progressMap = progressMap; + this.maxProgress = maxProgress; + + this.threshold = Settings.getOnProgressThreshold(); + } + + private void addProgress(long size) { + if (currentRequestProgress != null) { + currentRequestProgress.addProgress(size); + } + + batchProgress += size; + + if (batchProgress >= lastReportedProgress + threshold || batchProgress >= maxProgress) { + reportBatchProgress(); + } + } + + private void reportBatchProgress() { + if (batchProgress > lastReportedProgress) { + for (RequestBatch.Callback callback : requests.getCallbacks()) { + if (callback instanceof RequestBatch.OnProgressCallback) { + final Handler callbackHandler = requests.getCallbackHandler(); + + // Keep copies to avoid threading issues + final RequestBatch.OnProgressCallback progressCallback = (RequestBatch.OnProgressCallback) callback; + if (callbackHandler == null) { + progressCallback.onBatchProgress(requests, batchProgress, maxProgress); + } else { + callbackHandler.post(new Runnable() { + @Override + public void run() { + progressCallback.onBatchProgress(requests, batchProgress, maxProgress); + } + }); + } + } + } + + lastReportedProgress = batchProgress; + } + } + + public void setCurrentRequest(Request request) { + currentRequestProgress = request != null? progressMap.get(request) : null; + } + + long getBatchProgress() { + return batchProgress; + } + + long getMaxProgress() { + return maxProgress; + } + + @Override + public void write(byte[] buffer) throws IOException { + out.write(buffer); + addProgress(buffer.length); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + out.write(buffer, offset, length); + addProgress(length); + } + + @Override + public void write(int oneByte) throws IOException { + out.write(oneByte); + addProgress(1); + } + + @Override + public void close() throws IOException { + super.close(); + + for (RequestProgress p : progressMap.values()) { + p.reportProgress(); + } + + reportBatchProgress(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/Request.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Request.java new file mode 100644 index 0000000000..9b300e164e --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Request.java @@ -0,0 +1,2522 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import android.graphics.Bitmap; +import android.location.Location; +import android.net.Uri; +import android.os.*; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import com.facebook.internal.*; +import com.facebook.model.*; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A single request to be sent to the Facebook Platform through either the Graph API or REST API. The Request class provides functionality + * relating to serializing and deserializing requests and responses, making calls in batches (with a single round-trip + * to the service) and making calls asynchronously. + * + * The particular service endpoint that a request targets is determined by either a graph path (see the + * {@link #setGraphPath(String) setGraphPath} method) or a REST method name (see the {@link #setRestMethod(String) + * setRestMethod} method); a single request may not target both. + * + * A Request can be executed either anonymously or representing an authenticated user. In the former case, no Session + * needs to be specified, while in the latter, a Session that is in an opened state must be provided. If requests are + * executed in a batch, a Facebook application ID must be associated with the batch, either by supplying a Session for + * at least one of the requests in the batch (the first one found in the batch will be used) or by calling the + * {@link #setDefaultBatchApplicationId(String) setDefaultBatchApplicationId} method. + * + * After completion of a request, its Session, if any, will be checked to determine if its Facebook access token needs + * to be extended; if so, a request to extend it will be issued in the background. + */ +public class Request { + /** + * The maximum number of requests that can be submitted in a single batch. This limit is enforced on the service + * side by the Facebook platform, not by the Request class. + */ + public static final int MAXIMUM_BATCH_SIZE = 50; + + public static final String TAG = Request.class.getSimpleName(); + + private static final String ME = "me"; + private static final String MY_FRIENDS = "me/friends"; + private static final String MY_PHOTOS = "me/photos"; + private static final String MY_VIDEOS = "me/videos"; + private static final String VIDEOS_SUFFIX = "/videos"; + private static final String SEARCH = "search"; + private static final String MY_FEED = "me/feed"; + private static final String MY_STAGING_RESOURCES = "me/staging_resources"; + private static final String MY_OBJECTS_FORMAT = "me/objects/%s"; + private static final String MY_ACTION_FORMAT = "me/%s"; + + private static final String USER_AGENT_BASE = "FBAndroidSDK"; + private static final String USER_AGENT_HEADER = "User-Agent"; + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language"; + + // Parameter names/values + private static final String PICTURE_PARAM = "picture"; + private static final String FORMAT_PARAM = "format"; + private static final String FORMAT_JSON = "json"; + private static final String SDK_PARAM = "sdk"; + private static final String SDK_ANDROID = "android"; + private static final String ACCESS_TOKEN_PARAM = "access_token"; + private static final String BATCH_ENTRY_NAME_PARAM = "name"; + private static final String BATCH_ENTRY_OMIT_RESPONSE_ON_SUCCESS_PARAM = "omit_response_on_success"; + private static final String BATCH_ENTRY_DEPENDS_ON_PARAM = "depends_on"; + private static final String BATCH_APP_ID_PARAM = "batch_app_id"; + private static final String BATCH_RELATIVE_URL_PARAM = "relative_url"; + private static final String BATCH_BODY_PARAM = "body"; + private static final String BATCH_METHOD_PARAM = "method"; + private static final String BATCH_PARAM = "batch"; + private static final String ATTACHMENT_FILENAME_PREFIX = "file"; + private static final String ATTACHED_FILES_PARAM = "attached_files"; + private static final String ISO_8601_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String STAGING_PARAM = "file"; + private static final String OBJECT_PARAM = "object"; + + private static final String MIME_BOUNDARY = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f"; + + private static String defaultBatchApplicationId; + + private static Pattern versionPattern = Pattern.compile("^v\\d+\\.\\d+/.*"); + + private Session session; + private HttpMethod httpMethod; + private String graphPath; + private GraphObject graphObject; + private String restMethod; + private String batchEntryName; + private String batchEntryDependsOn; + private boolean batchEntryOmitResultOnSuccess = true; + private Bundle parameters; + private Callback callback; + private String overriddenURL; + private Object tag; + private String version; + + /** + * Constructs a request without a session, graph path, or any other parameters. + */ + public Request() { + this(null, null, null, null, null); + } + + /** + * Constructs a request with a Session to retrieve a particular graph path. A Session need not be provided, in which + * case the request is sent without an access token and thus is not executed in the context of any particular user. + * Only certain graph requests can be expected to succeed in this case. If a Session is provided, it must be in an + * opened state or the request will fail. + * + * @param session + * the Session to use, or null + * @param graphPath + * the graph path to retrieve + */ + public Request(Session session, String graphPath) { + this(session, graphPath, null, null, null); + } + + /** + * Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be + * provided, in which case the request is sent without an access token and thus is not executed in the context of + * any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is + * provided, it must be in an opened state or the request will fail. + * + * Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted. + * + * @param session + * the Session to use, or null + * @param graphPath + * the graph path to retrieve, create, or delete + * @param parameters + * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers, + * Bitmaps, Dates, or Byte arrays. + * @param httpMethod + * the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET) + */ + public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod) { + this(session, graphPath, parameters, httpMethod, null); + } + + /** + * Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be + * provided, in which case the request is sent without an access token and thus is not executed in the context of + * any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is + * provided, it must be in an opened state or the request will fail. + * + * Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted. + * + * @param session + * the Session to use, or null + * @param graphPath + * the graph path to retrieve, create, or delete + * @param parameters + * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers, + * Bitmaps, Dates, or Byte arrays. + * @param httpMethod + * the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET) + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + */ + public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback) { + this(session, graphPath, parameters, httpMethod, callback, null); + } + + /** + * Constructs a request with a specific Session, graph path, parameters, and HTTP method. A Session need not be + * provided, in which case the request is sent without an access token and thus is not executed in the context of + * any particular user. Only certain graph requests can be expected to succeed in this case. If a Session is + * provided, it must be in an opened state or the request will fail. + * + * Depending on the httpMethod parameter, the object at the graph path may be retrieved, created, or deleted. + * + * @param session + * the Session to use, or null + * @param graphPath + * the graph path to retrieve, create, or delete + * @param parameters + * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers, + * Bitmaps, Dates, or Byte arrays. + * @param httpMethod + * the {@link HttpMethod} to use for the request, or null for default (HttpMethod.GET) + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @param version + * the version of the Graph API + */ + public Request(Session session, String graphPath, Bundle parameters, HttpMethod httpMethod, Callback callback, String version) { + this.session = session; + this.graphPath = graphPath; + this.callback = callback; + this.version = version; + + setHttpMethod(httpMethod); + + if (parameters != null) { + this.parameters = new Bundle(parameters); + } else { + this.parameters = new Bundle(); + } + + if (this.version == null) { + this.version = ServerProtocol.getAPIVersion(); + } + } + + Request(Session session, URL overriddenURL) { + this.session = session; + this.overriddenURL = overriddenURL.toString(); + + setHttpMethod(HttpMethod.GET); + + this.parameters = new Bundle(); + } + + /** + * Creates a new Request configured to post a GraphObject to a particular graph path, to either create or update the + * object at that path. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param graphPath + * the graph path to retrieve, create, or delete + * @param graphObject + * the GraphObject to create or update + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newPostRequest(Session session, String graphPath, GraphObject graphObject, Callback callback) { + Request request = new Request(session, graphPath, null, HttpMethod.POST , callback); + request.setGraphObject(graphObject); + return request; + } + + /** + * Creates a new Request configured to make a call to the Facebook REST API. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param restMethod + * the method in the Facebook REST API to execute + * @param parameters + * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers, + * Bitmaps, Dates, or Byte arrays. + * @param httpMethod + * the HTTP method to use for the request; must be one of GET, POST, or DELETE + * @return a Request that is ready to execute + */ + public static Request newRestRequest(Session session, String restMethod, Bundle parameters, HttpMethod httpMethod) { + Request request = new Request(session, null, parameters, httpMethod); + request.setRestMethod(restMethod); + return request; + } + + /** + * Creates a new Request configured to retrieve a user's own profile. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newMeRequest(Session session, final GraphUserCallback callback) { + Callback wrapper = new Callback() { + @Override + public void onCompleted(Response response) { + if (callback != null) { + callback.onCompleted(response.getGraphObjectAs(GraphUser.class), response); + } + } + }; + return new Request(session, ME, null, null, wrapper); + } + + /** + * Creates a new Request configured to retrieve a user's friend list. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newMyFriendsRequest(Session session, final GraphUserListCallback callback) { + Callback wrapper = new Callback() { + @Override + public void onCompleted(Response response) { + if (callback != null) { + callback.onCompleted(typedListFromResponse(response, GraphUser.class), response); + } + } + }; + return new Request(session, MY_FRIENDS, null, null, wrapper); + } + + /** + * Creates a new Request configured to upload a photo to the user's default photo album. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param image + * the image to upload + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUploadPhotoRequest(Session session, Bitmap image, Callback callback) { + Bundle parameters = new Bundle(1); + parameters.putParcelable(PICTURE_PARAM, image); + + return new Request(session, MY_PHOTOS, parameters, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to upload a photo to the user's default photo album. The photo + * will be read from the specified stream. + * + * @param session the Session to use, or null; if non-null, the session must be in an opened state + * @param file the file containing the photo to upload + * @param callback a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUploadPhotoRequest(Session session, File file, + Callback callback) throws FileNotFoundException { + ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + Bundle parameters = new Bundle(1); + parameters.putParcelable(PICTURE_PARAM, descriptor); + + return new Request(session, MY_PHOTOS, parameters, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to upload a photo to the user's default photo album. The photo + * will be read from the specified file descriptor. + * + * @param session the Session to use, or null; if non-null, the session must be in an opened state + * @param file the file to upload + * @param callback a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUploadVideoRequest(Session session, File file, + Callback callback) throws FileNotFoundException { + ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + Bundle parameters = new Bundle(1); + parameters.putParcelable(file.getName(), descriptor); + + return new Request(session, MY_VIDEOS, parameters, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to retrieve a particular graph path. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param graphPath + * the graph path to retrieve + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newGraphPathRequest(Session session, String graphPath, Callback callback) { + return new Request(session, graphPath, null, null, callback); + } + + /** + * Creates a new Request that is configured to perform a search for places near a specified location via the Graph + * API. At least one of location or searchText must be specified. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param location + * the location around which to search; only the latitude and longitude components of the location are + * meaningful + * @param radiusInMeters + * the radius around the location to search, specified in meters; this is ignored if + * no location is specified + * @param resultsLimit + * the maximum number of results to return + * @param searchText + * optional text to search for as part of the name or type of an object + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + * + * @throws FacebookException If neither location nor searchText is specified + */ + public static Request newPlacesSearchRequest(Session session, Location location, int radiusInMeters, + int resultsLimit, String searchText, final GraphPlaceListCallback callback) { + if (location == null && Utility.isNullOrEmpty(searchText)) { + throw new FacebookException("Either location or searchText must be specified."); + } + + Bundle parameters = new Bundle(5); + parameters.putString("type", "place"); + parameters.putInt("limit", resultsLimit); + if (location != null) { + parameters.putString("center", + String.format(Locale.US, "%f,%f", location.getLatitude(), location.getLongitude())); + parameters.putInt("distance", radiusInMeters); + } + if (!Utility.isNullOrEmpty(searchText)) { + parameters.putString("q", searchText); + } + + Callback wrapper = new Callback() { + @Override + public void onCompleted(Response response) { + if (callback != null) { + callback.onCompleted(typedListFromResponse(response, GraphPlace.class), response); + } + } + }; + + return new Request(session, SEARCH, parameters, HttpMethod.GET, wrapper); + } + + /** + * Creates a new Request configured to post a status update to a user's feed. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param message + * the text of the status update + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newStatusUpdateRequest(Session session, String message, Callback callback) { + return newStatusUpdateRequest(session, message, (String)null, null, callback); + } + + /** + * Creates a new Request configured to post a status update to a user's feed. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param message + * the text of the status update + * @param placeId + * an optional place id to associate with the post + * @param tagIds + * an optional list of user ids to tag in the post + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + private static Request newStatusUpdateRequest(Session session, String message, String placeId, List tagIds, + Callback callback) { + + Bundle parameters = new Bundle(); + parameters.putString("message", message); + + if (placeId != null) { + parameters.putString("place", placeId); + } + + if (tagIds != null && tagIds.size() > 0) { + String tags = TextUtils.join(",", tagIds); + parameters.putString("tags", tags); + } + + return new Request(session, MY_FEED, parameters, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to post a status update to a user's feed. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param message + * the text of the status update + * @param place + * an optional place to associate with the post + * @param tags + * an optional list of users to tag in the post + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newStatusUpdateRequest(Session session, String message, GraphPlace place, + List tags, Callback callback) { + + List tagIds = null; + if (tags != null) { + tagIds = new ArrayList(tags.size()); + for (GraphUser tag: tags) { + tagIds.add(tag.getId()); + } + } + String placeId = place == null ? null : place.getId(); + return newStatusUpdateRequest(session, message, placeId, tagIds, callback); + } + + /** + * Creates a new Request configured to retrieve an App User ID for the app's Facebook user. Callers + * will send this ID back to their own servers, collect up a set to create a Facebook Custom Audience with, + * and then use the resultant Custom Audience to target ads. + *

+ * The GraphObject in the response will include an "custom_audience_third_party_id" property, with the value + * being the ID retrieved. This ID is an encrypted encoding of the Facebook user's ID and the + * invoking Facebook app ID. Multiple calls with the same user will return different IDs, thus these IDs cannot be + * used to correlate behavior across devices or applications, and are only meaningful when sent back to Facebook + * for creating Custom Audiences. + *

+ * The ID retrieved represents the Facebook user identified in the following way: if the specified session + * (or activeSession if the specified session is `null`) is open, the ID will represent the user associated with + * the activeSession; otherwise the ID will represent the user logged into the native Facebook app on the device. + * A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into + * it, or c) the app has previously called + * {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} with `true` for this user. + * + * @param session + * the Session to issue the Request on, or null; if non-null, the session must be in an opened state. + * If there is no logged-in Facebook user, null is the expected choice. + * @param context + * the Application context from which the app ID will be pulled, and from which the 'attribution ID' + * for the Facebook user is determined. If there has been no app ID set, an exception will be thrown. + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions. + * The GraphObject in the Response will contain a "custom_audience_third_party_id" property that + * represents the user as described above. + * @return a Request that is ready to execute + */ + public static Request newCustomAudienceThirdPartyIdRequest(Session session, Context context, Callback callback) { + return newCustomAudienceThirdPartyIdRequest(session, context, null, callback); + } + + /** + * Creates a new Request configured to retrieve an App User ID for the app's Facebook user. Callers + * will send this ID back to their own servers, collect up a set to create a Facebook Custom Audience with, + * and then use the resultant Custom Audience to target ads. + *

+ * The GraphObject in the response will include an "custom_audience_third_party_id" property, with the value + * being the ID retrieved. This ID is an encrypted encoding of the Facebook user's ID and the + * invoking Facebook app ID. Multiple calls with the same user will return different IDs, thus these IDs cannot be + * used to correlate behavior across devices or applications, and are only meaningful when sent back to Facebook + * for creating Custom Audiences. + *

+ * The ID retrieved represents the Facebook user identified in the following way: if the specified session + * (or activeSession if the specified session is `null`) is open, the ID will represent the user associated with + * the activeSession; otherwise the ID will represent the user logged into the native Facebook app on the device. + * A `null` ID will be provided into the callback if a) there is no native Facebook app, b) no one is logged into + * it, or c) the app has previously called + * {@link Settings#setLimitEventAndDataUsage(android.content.Context, boolean)} ;} with `true` for this user. + * + * @param session + * the Session to issue the Request on, or null; if non-null, the session must be in an opened state. + * If there is no logged-in Facebook user, null is the expected choice. + * @param context + * the Application context from which the app ID will be pulled, and from which the 'attribution ID' + * for the Facebook user is determined. If there has been no app ID set, an exception will be thrown. + * @param applicationId + * explicitly specified Facebook App ID. If null, and there's a valid session, then the application ID + * from the session will be used, otherwise the application ID from metadata will be used. + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions. + * The GraphObject in the Response will contain a "custom_audience_third_party_id" property that + * represents the user as described above. + * @return a Request that is ready to execute + */ + public static Request newCustomAudienceThirdPartyIdRequest(Session session, + Context context, String applicationId, Callback callback) { + + // if provided session or activeSession is opened, use it. + if (session == null) { + session = Session.getActiveSession(); + } + + if (session != null && !session.isOpened()) { + session = null; + } + + if (applicationId == null) { + if (session != null) { + applicationId = session.getApplicationId(); + } else { + applicationId = Utility.getMetadataApplicationId(context); + } + } + + if (applicationId == null) { + throw new FacebookException("Facebook App ID cannot be determined"); + } + + String endpoint = applicationId + "/custom_audience_third_party_id"; + AttributionIdentifiers attributionIdentifiers = AttributionIdentifiers.getAttributionIdentifiers(context); + Bundle parameters = new Bundle(); + + if (session == null) { + // Only use the attributionID if we don't have an open session. If we do have an open session, then + // the user token will be used to identify the user, and is more reliable than the attributionID. + String udid = attributionIdentifiers.getAttributionId() != null + ? attributionIdentifiers.getAttributionId() + : attributionIdentifiers.getAndroidAdvertiserId(); + if (attributionIdentifiers.getAttributionId() != null) { + parameters.putString("udid", udid); + } + } + + // Server will choose to not provide the App User ID in the event that event usage has been limited for + // this user for this app. + if (Settings.getLimitEventAndDataUsage(context) || attributionIdentifiers.isTrackingLimited()) { + parameters.putString("limit_event_usage", "1"); + } + + return new Request(session, endpoint, parameters, HttpMethod.GET, callback); + } + + /** + * Creates a new Request configured to upload an image to create a staging resource. Staging resources + * allow you to post binary data such as images, in preparation for a post of an Open Graph object or action + * which references the image. The URI returned when uploading a staging resource may be passed as the image + * property for an Open Graph object or action. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param image + * the image to upload + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUploadStagingResourceWithImageRequest(Session session, + Bitmap image, Callback callback) { + Bundle parameters = new Bundle(1); + parameters.putParcelable(STAGING_PARAM, image); + + return new Request(session, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to upload an image to create a staging resource. Staging resources + * allow you to post binary data such as images, in preparation for a post of an Open Graph object or action + * which references the image. The URI returned when uploading a staging resource may be passed as the image + * property for an Open Graph object or action. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param file + * the file containing the image to upload + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUploadStagingResourceWithImageRequest(Session session, + File file, Callback callback) throws FileNotFoundException { + ParcelFileDescriptor descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + ParcelFileDescriptorWithMimeType descriptorWithMimeType = new ParcelFileDescriptorWithMimeType(descriptor, "image/png"); + Bundle parameters = new Bundle(1); + parameters.putParcelable(STAGING_PARAM, descriptorWithMimeType); + + return new Request(session, MY_STAGING_RESOURCES, parameters, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to create a user owned Open Graph object. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param openGraphObject + * the Open Graph object to create; must not be null, and must have a non-empty type and title + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newPostOpenGraphObjectRequest(Session session, + OpenGraphObject openGraphObject, Callback callback) { + if (openGraphObject == null) { + throw new FacebookException("openGraphObject cannot be null"); + } + if (Utility.isNullOrEmpty(openGraphObject.getType())) { + throw new FacebookException("openGraphObject must have non-null 'type' property"); + } + if (Utility.isNullOrEmpty(openGraphObject.getTitle())) { + throw new FacebookException("openGraphObject must have non-null 'title' property"); + } + + String path = String.format(MY_OBJECTS_FORMAT, openGraphObject.getType()); + Bundle bundle = new Bundle(); + bundle.putString(OBJECT_PARAM, openGraphObject.getInnerJSONObject().toString()); + return new Request(session, path, bundle, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to create a user owned Open Graph object. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param type + * the fully-specified Open Graph object type (e.g., my_app_namespace:my_object_name); must not be null + * @param title + * the title of the Open Graph object; must not be null + * @param imageUrl + * the link to an image to be associated with the Open Graph object; may be null + * @param url + * the url to be associated with the Open Graph object; may be null + * @param description + * the description to be associated with the object; may be null + * @param objectProperties + * any additional type-specific properties for the Open Graph object; may be null + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions; + * may be null + * @return a Request that is ready to execute + */ + public static Request newPostOpenGraphObjectRequest(Session session, String type, String title, String imageUrl, + String url, String description, GraphObject objectProperties, Callback callback) { + OpenGraphObject openGraphObject = OpenGraphObject.Factory.createForPost(OpenGraphObject.class, type, title, + imageUrl, url, description); + if (objectProperties != null) { + openGraphObject.setData(objectProperties); + } + + return newPostOpenGraphObjectRequest(session, openGraphObject, callback); + } + + /** + * Creates a new Request configured to publish an Open Graph action. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param openGraphAction + * the Open Graph object to create; must not be null, and must have a non-empty 'type' + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newPostOpenGraphActionRequest(Session session, OpenGraphAction openGraphAction, + Callback callback) { + if (openGraphAction == null) { + throw new FacebookException("openGraphAction cannot be null"); + } + if (Utility.isNullOrEmpty(openGraphAction.getType())) { + throw new FacebookException("openGraphAction must have non-null 'type' property"); + } + + String path = String.format(MY_ACTION_FORMAT, openGraphAction.getType()); + return newPostRequest(session, path, openGraphAction, callback); + } + + /** + * Creates a new Request configured to delete a resource through the Graph API. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param id + * the id of the object to delete + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newDeleteObjectRequest(Session session, String id, Callback callback) { + return new Request(session, id, null, HttpMethod.DELETE, callback); + } + + /** + * Creates a new Request configured to update a user owned Open Graph object. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param openGraphObject + * the Open Graph object to update, which must have a valid 'id' property + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUpdateOpenGraphObjectRequest(Session session, OpenGraphObject openGraphObject, + Callback callback) { + if (openGraphObject == null) { + throw new FacebookException("openGraphObject cannot be null"); + } + + String path = openGraphObject.getId(); + if (path == null) { + throw new FacebookException("openGraphObject must have an id"); + } + + Bundle bundle = new Bundle(); + bundle.putString(OBJECT_PARAM, openGraphObject.getInnerJSONObject().toString()); + return new Request(session, path, bundle, HttpMethod.POST, callback); + } + + /** + * Creates a new Request configured to update a user owned Open Graph object. + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param id + * the id of the Open Graph object + * @param title + * the title of the Open Graph object + * @param imageUrl + * the link to an image to be associated with the Open Graph object + * @param url + * the url to be associated with the Open Graph object + * @param description + * the description to be associated with the object + * @param objectProperties + * any additional type-specific properties for the Open Graph object + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a Request that is ready to execute + */ + public static Request newUpdateOpenGraphObjectRequest(Session session, String id, String title, String imageUrl, + String url, String description, GraphObject objectProperties, Callback callback) { + OpenGraphObject openGraphObject = OpenGraphObject.Factory.createForPost(OpenGraphObject.class, null, title, + imageUrl, url, description); + openGraphObject.setId(id); + openGraphObject.setData(objectProperties); + + return newUpdateOpenGraphObjectRequest(session, openGraphObject, callback); + } + + /** + * Returns the GraphObject, if any, associated with this request. + * + * @return the GraphObject associated with this requeset, or null if there is none + */ + public final GraphObject getGraphObject() { + return this.graphObject; + } + + /** + * Sets the GraphObject associated with this request. This is meaningful only for POST requests. + * + * @param graphObject + * the GraphObject to upload along with this request + */ + public final void setGraphObject(GraphObject graphObject) { + this.graphObject = graphObject; + } + + /** + * Returns the graph path of this request, if any. + * + * @return the graph path of this request, or null if there is none + */ + public final String getGraphPath() { + return this.graphPath; + } + + /** + * Sets the graph path of this request. A graph path may not be set if a REST method has been specified. + * + * @param graphPath + * the graph path for this request + */ + public final void setGraphPath(String graphPath) { + this.graphPath = graphPath; + } + + /** + * Returns the {@link HttpMethod} to use for this request. + * + * @return the HttpMethod + */ + public final HttpMethod getHttpMethod() { + return this.httpMethod; + } + + /** + * Sets the {@link HttpMethod} to use for this request. + * + * @param httpMethod + * the HttpMethod, or null for the default (HttpMethod.GET). + */ + public final void setHttpMethod(HttpMethod httpMethod) { + if (overriddenURL != null && httpMethod != HttpMethod.GET) { + throw new FacebookException("Can't change HTTP method on request with overridden URL."); + } + this.httpMethod = (httpMethod != null) ? httpMethod : HttpMethod.GET; + } + + /** + * Returns the version of the API that this request will use. By default this is the current API at the time + * the SDK is released. + * + * @return the version that this request will use + */ + public final String getVersion() { + return this.version; + } + + /** + * Set the version to use for this request. By default the version will be the current API at the time the SDK + * is released. Only use this if you need to explicitly override. + * + * @param version The version to use. Should look like "v2.0" + */ + public final void setVersion(String version) { + this.version = version; + } + + /** + * Returns the parameters for this request. + * + * @return the parameters + */ + public final Bundle getParameters() { + return this.parameters; + } + + /** + * Sets the parameters for this request. + * + * @param parameters + * the parameters + */ + public final void setParameters(Bundle parameters) { + this.parameters = parameters; + } + + /** + * Returns the REST method to call for this request. + * + * @return the REST method + */ + public final String getRestMethod() { + return this.restMethod; + } + + /** + * Sets the REST method to call for this request. A REST method may not be set if a graph path has been specified. + * + * @param restMethod + * the REST method to call + */ + public final void setRestMethod(String restMethod) { + this.restMethod = restMethod; + } + + /** + * Returns the Session associated with this request. + * + * @return the Session associated with this request, or null if none has been specified + */ + public final Session getSession() { + return this.session; + } + + /** + * Sets the Session to use for this request. The Session does not need to be opened at the time it is specified, but + * it must be opened by the time the request is executed. + * + * @param session + * the Session to use for this request + */ + public final void setSession(Session session) { + this.session = session; + } + + /** + * Returns the name of this request's entry in a batched request. + * + * @return the name of this request's batch entry, or null if none has been specified + */ + public final String getBatchEntryName() { + return this.batchEntryName; + } + + /** + * Sets the name of this request's entry in a batched request. This value is only used if this request is submitted + * as part of a batched request. It can be used to specified dependencies between requests. See Batch Requests in the Graph API + * documentation for more details. + * + * @param batchEntryName + * the name of this request's entry in a batched request, which must be unique within a particular batch + * of requests + */ + public final void setBatchEntryName(String batchEntryName) { + this.batchEntryName = batchEntryName; + } + + /** + * Returns the name of the request that this request entry explicitly depends on in a batched request. + * + * @return the name of this request's dependency, or null if none has been specified + */ + public final String getBatchEntryDependsOn() { + return this.batchEntryDependsOn; + } + + /** + * Sets the name of the request entry that this request explicitly depends on in a batched request. This value is + * only used if this request is submitted as part of a batched request. It can be used to specified dependencies + * between requests. See Batch Requests in + * the Graph API documentation for more details. + * + * @param batchEntryDependsOn + * the name of the request entry that this entry depends on in a batched request + */ + public final void setBatchEntryDependsOn(String batchEntryDependsOn) { + this.batchEntryDependsOn = batchEntryDependsOn; + } + + + /** + * Returns whether or not this batch entry will return a response if it is successful. Only applies if another + * request entry in the batch specifies this entry as a dependency. + * + * @return the name of this request's dependency, or null if none has been specified + */ + public final boolean getBatchEntryOmitResultOnSuccess() { + return this.batchEntryOmitResultOnSuccess; + } + + /** + * Sets whether or not this batch entry will return a response if it is successful. Only applies if another + * request entry in the batch specifies this entry as a dependency. See + * Batch Requests in the Graph API + * documentation for more details. + * + * @param batchEntryOmitResultOnSuccess + * the name of the request entry that this entry depends on in a batched request + */ + public final void setBatchEntryOmitResultOnSuccess(boolean batchEntryOmitResultOnSuccess) { + this.batchEntryOmitResultOnSuccess = batchEntryOmitResultOnSuccess; + } + + /** + * Gets the default Facebook application ID that will be used to submit batched requests if none of those requests + * specifies a Session. Batched requests require an application ID, so either at least one request in a batch must + * specify a Session or the application ID must be specified explicitly. + * + * @return the Facebook application ID to use for batched requests if none can be determined + */ + public static final String getDefaultBatchApplicationId() { + return Request.defaultBatchApplicationId; + } + + /** + * Sets the default application ID that will be used to submit batched requests if none of those requests specifies + * a Session. Batched requests require an application ID, so either at least one request in a batch must specify a + * Session or the application ID must be specified explicitly. + * + * @param applicationId + * the Facebook application ID to use for batched requests if none can be determined + */ + public static final void setDefaultBatchApplicationId(String applicationId) { + defaultBatchApplicationId = applicationId; + } + + /** + * Returns the callback which will be called when the request finishes. + * + * @return the callback + */ + public final Callback getCallback() { + return callback; + } + + /** + * Sets the callback which will be called when the request finishes. + * + * @param callback + * the callback + */ + public final void setCallback(Callback callback) { + this.callback = callback; + } + + /** + * Sets the tag on the request; this is an application-defined object that can be used to distinguish + * between different requests. Its value has no effect on the execution of the request. + * + * @param tag an object to serve as a tag, or null + */ + public final void setTag(Object tag) { + this.tag = tag; + } + + /** + * Gets the tag on the request; this is an application-defined object that can be used to distinguish + * between different requests. Its value has no effect on the execution of the request. + * + * @return an object that serves as a tag, or null + */ + public final Object getTag() { + return tag; + } + + /** + * Starts a new Request configured to post a GraphObject to a particular graph path, to either create or update the + * object at that path. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newPostRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param graphPath + * the graph path to retrieve, create, or delete + * @param graphObject + * the GraphObject to create or update + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executePostRequestAsync(Session session, String graphPath, GraphObject graphObject, + Callback callback) { + return newPostRequest(session, graphPath, graphObject, callback).executeAsync(); + } + + /** + * Starts a new Request configured to make a call to the Facebook REST API. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newRestRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param restMethod + * the method in the Facebook REST API to execute + * @param parameters + * additional parameters to pass along with the Graph API request; parameters must be Strings, Numbers, + * Bitmaps, Dates, or Byte arrays. + * @param httpMethod + * the HTTP method to use for the request; must be one of GET, POST, or DELETE + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeRestRequestAsync(Session session, String restMethod, Bundle parameters, + HttpMethod httpMethod) { + return newRestRequest(session, restMethod, parameters, httpMethod).executeAsync(); + } + + /** + * Starts a new Request configured to retrieve a user's own profile. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newMeRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeMeRequestAsync(Session session, GraphUserCallback callback) { + return newMeRequest(session, callback).executeAsync(); + } + + /** + * Starts a new Request configured to retrieve a user's friend list. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newMyFriendsRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeMyFriendsRequestAsync(Session session, GraphUserListCallback callback) { + return newMyFriendsRequest(session, callback).executeAsync(); + } + + /** + * Starts a new Request configured to upload a photo to the user's default photo album. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newUploadPhotoRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param image + * the image to upload + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeUploadPhotoRequestAsync(Session session, Bitmap image, Callback callback) { + return newUploadPhotoRequest(session, image, callback).executeAsync(); + } + + /** + * Starts a new Request configured to upload a photo to the user's default photo album. The photo + * will be read from the specified stream. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newUploadPhotoRequest(...).executeAsync(); + * + * @param session the Session to use, or null; if non-null, the session must be in an opened state + * @param file the file containing the photo to upload + * @param callback a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeUploadPhotoRequestAsync(Session session, File file, + Callback callback) throws FileNotFoundException { + return newUploadPhotoRequest(session, file, callback).executeAsync(); + } + + /** + * Starts a new Request configured to retrieve a particular graph path. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newGraphPathRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param graphPath + * the graph path to retrieve + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeGraphPathRequestAsync(Session session, String graphPath, Callback callback) { + return newGraphPathRequest(session, graphPath, callback).executeAsync(); + } + + /** + * Starts a new Request that is configured to perform a search for places near a specified location via the Graph + * API. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newPlacesSearchRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param location + * the location around which to search; only the latitude and longitude components of the location are + * meaningful + * @param radiusInMeters + * the radius around the location to search, specified in meters + * @param resultsLimit + * the maximum number of results to return + * @param searchText + * optional text to search for as part of the name or type of an object + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + * + * @throws FacebookException If neither location nor searchText is specified + */ + @Deprecated + public static RequestAsyncTask executePlacesSearchRequestAsync(Session session, Location location, + int radiusInMeters, int resultsLimit, String searchText, GraphPlaceListCallback callback) { + return newPlacesSearchRequest(session, location, radiusInMeters, resultsLimit, searchText, callback) + .executeAsync(); + } + + /** + * Starts a new Request configured to post a status update to a user's feed. + *

+ * This should only be called from the UI thread. + * + * This method is deprecated. Prefer to call Request.newStatusUpdateRequest(...).executeAsync(); + * + * @param session + * the Session to use, or null; if non-null, the session must be in an opened state + * @param message + * the text of the status update + * @param callback + * a callback that will be called when the request is completed to handle success or error conditions + * @return a RequestAsyncTask that is executing the request + */ + @Deprecated + public static RequestAsyncTask executeStatusUpdateRequestAsync(Session session, String message, Callback callback) { + return newStatusUpdateRequest(session, message, callback).executeAsync(); + } + + /** + * Executes this request and returns the response. + *

+ * This should only be called if you have transitioned off the UI thread. + * + * @return the Response object representing the results of the request + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + * @throws IllegalArgumentException + */ + public final Response executeAndWait() { + return Request.executeAndWait(this); + } + + /** + * Executes this request and returns the response. + *

+ * This should only be called from the UI thread. + * + * @return a RequestAsyncTask that is executing the request + * + * @throws IllegalArgumentException + */ + public final RequestAsyncTask executeAsync() { + return Request.executeBatchAsync(this); + } + + /** + * Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed + * explicitly by the caller. + * + * @param requests + * one or more Requests to serialize + * @return an HttpURLConnection which is ready to execute + * + * @throws FacebookException + * If any of the requests in the batch are badly constructed or if there are problems + * contacting the service + * @throws IllegalArgumentException if the passed in array is zero-length + * @throws NullPointerException if the passed in array or any of its contents are null + */ + public static HttpURLConnection toHttpConnection(Request... requests) { + return toHttpConnection(Arrays.asList(requests)); + } + + /** + * Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed + * explicitly by the caller. + * + * @param requests + * one or more Requests to serialize + * @return an HttpURLConnection which is ready to execute + * + * @throws FacebookException + * If any of the requests in the batch are badly constructed or if there are problems + * contacting the service + * @throws IllegalArgumentException if the passed in collection is empty + * @throws NullPointerException if the passed in collection or any of its contents are null + */ + public static HttpURLConnection toHttpConnection(Collection requests) { + Validate.notEmptyAndContainsNoNulls(requests, "requests"); + + return toHttpConnection(new RequestBatch(requests)); + } + + + /** + * Serializes one or more requests but does not execute them. The resulting HttpURLConnection can be executed + * explicitly by the caller. + * + * @param requests + * a RequestBatch to serialize + * @return an HttpURLConnection which is ready to execute + * + * @throws FacebookException + * If any of the requests in the batch are badly constructed or if there are problems + * contacting the service + * @throws IllegalArgumentException + */ + public static HttpURLConnection toHttpConnection(RequestBatch requests) { + + for (Request request : requests) { + request.validate(); + } + + URL url = null; + try { + if (requests.size() == 1) { + // Single request case. + Request request = requests.get(0); + // In the non-batch case, the URL we use really is the same one returned by getUrlForSingleRequest. + url = new URL(request.getUrlForSingleRequest()); + } else { + // Batch case -- URL is just the graph API base, individual request URLs are serialized + // as relative_url parameters within each batch entry. + url = new URL(ServerProtocol.getGraphUrlBase()); + } + } catch (MalformedURLException e) { + throw new FacebookException("could not construct URL for request", e); + } + + HttpURLConnection connection; + try { + connection = createConnection(url); + + serializeToUrlConnection(requests, connection); + } catch (IOException e) { + throw new FacebookException("could not construct request body", e); + } catch (JSONException e) { + throw new FacebookException("could not construct request body", e); + } + + return connection; + } + + /** + * Executes a single request on the current thread and returns the response. + *

+ * This should only be used if you have transitioned off the UI thread. + * + * @param request + * the Request to execute + * + * @return the Response object representing the results of the request + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + */ + public static Response executeAndWait(Request request) { + List responses = executeBatchAndWait(request); + + if (responses == null || responses.size() != 1) { + throw new FacebookException("invalid state: expected a single response"); + } + + return responses.get(0); + } + + /** + * Executes requests on the current thread as a single batch and returns the responses. + *

+ * This should only be used if you have transitioned off the UI thread. + * + * @param requests + * the Requests to execute + * + * @return a list of Response objects representing the results of the requests; responses are returned in the same + * order as the requests were specified. + * + * @throws NullPointerException + * In case of a null request + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + */ + public static List executeBatchAndWait(Request... requests) { + Validate.notNull(requests, "requests"); + + return executeBatchAndWait(Arrays.asList(requests)); + } + + /** + * Executes requests as a single batch on the current thread and returns the responses. + *

+ * This should only be used if you have transitioned off the UI thread. + * + * @param requests + * the Requests to execute + * + * @return a list of Response objects representing the results of the requests; responses are returned in the same + * order as the requests were specified. + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + */ + public static List executeBatchAndWait(Collection requests) { + return executeBatchAndWait(new RequestBatch(requests)); + } + + /** + * Executes requests on the current thread as a single batch and returns the responses. + *

+ * This should only be used if you have transitioned off the UI thread. + * + * @param requests + * the batch of Requests to execute + * + * @return a list of Response objects representing the results of the requests; responses are returned in the same + * order as the requests were specified. + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + * @throws IllegalArgumentException if the passed in RequestBatch is empty + * @throws NullPointerException if the passed in RequestBatch or any of its contents are null + */ + public static List executeBatchAndWait(RequestBatch requests) { + Validate.notEmptyAndContainsNoNulls(requests, "requests"); + + HttpURLConnection connection = null; + try { + connection = toHttpConnection(requests); + } catch (Exception ex) { + List responses = Response.constructErrorResponses(requests.getRequests(), null, new FacebookException(ex)); + runCallbacks(requests, responses); + return responses; + } + + List responses = executeConnectionAndWait(connection, requests); + return responses; + } + + /** + * Executes requests as a single batch asynchronously. This function will return immediately, and the requests will + * be processed on a separate thread. In order to process results of a request, or determine whether a request + * succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method). + *

+ * This should only be called from the UI thread. + * + * @param requests + * the Requests to execute + * @return a RequestAsyncTask that is executing the request + * + * @throws NullPointerException + * If a null request is passed in + */ + public static RequestAsyncTask executeBatchAsync(Request... requests) { + Validate.notNull(requests, "requests"); + + return executeBatchAsync(Arrays.asList(requests)); + } + + /** + * Executes requests as a single batch asynchronously. This function will return immediately, and the requests will + * be processed on a separate thread. In order to process results of a request, or determine whether a request + * succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method). + *

+ * This should only be called from the UI thread. + * + * @param requests + * the Requests to execute + * @return a RequestAsyncTask that is executing the request + * + * @throws IllegalArgumentException if the passed in collection is empty + * @throws NullPointerException if the passed in collection or any of its contents are null + */ + public static RequestAsyncTask executeBatchAsync(Collection requests) { + return executeBatchAsync(new RequestBatch(requests)); + } + + /** + * Executes requests as a single batch asynchronously. This function will return immediately, and the requests will + * be processed on a separate thread. In order to process results of a request, or determine whether a request + * succeeded or failed, a callback must be specified (see the {@link #setCallback(Callback) setCallback} method). + *

+ * This should only be called from the UI thread. + * + * @param requests + * the RequestBatch to execute + * @return a RequestAsyncTask that is executing the request + * + * @throws IllegalArgumentException if the passed in RequestBatch is empty + * @throws NullPointerException if the passed in RequestBatch or any of its contents are null + */ + public static RequestAsyncTask executeBatchAsync(RequestBatch requests) { + Validate.notEmptyAndContainsNoNulls(requests, "requests"); + + RequestAsyncTask asyncTask = new RequestAsyncTask(requests); + asyncTask.executeOnSettingsExecutor(); + return asyncTask; + } + + /** + * Executes requests that have already been serialized into an HttpURLConnection. No validation is done that the + * contents of the connection actually reflect the serialized requests, so it is the caller's responsibility to + * ensure that it will correctly generate the desired responses. + *

+ * This should only be called if you have transitioned off the UI thread. + * + * @param connection + * the HttpURLConnection that the requests were serialized into + * @param requests + * the requests represented by the HttpURLConnection + * @return a list of Responses corresponding to the requests + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + */ + public static List executeConnectionAndWait(HttpURLConnection connection, Collection requests) { + return executeConnectionAndWait(connection, new RequestBatch(requests)); + } + + /** + * Executes requests that have already been serialized into an HttpURLConnection. No validation is done that the + * contents of the connection actually reflect the serialized requests, so it is the caller's responsibility to + * ensure that it will correctly generate the desired responses. + *

+ * This should only be called if you have transitioned off the UI thread. + * + * @param connection + * the HttpURLConnection that the requests were serialized into + * @param requests + * the RequestBatch represented by the HttpURLConnection + * @return a list of Responses corresponding to the requests + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + */ + public static List executeConnectionAndWait(HttpURLConnection connection, RequestBatch requests) { + List responses = Response.fromHttpConnection(connection, requests); + + Utility.disconnectQuietly(connection); + + int numRequests = requests.size(); + if (numRequests != responses.size()) { + throw new FacebookException(String.format("Received %d responses while expecting %d", responses.size(), + numRequests)); + } + + runCallbacks(requests, responses); + + // See if any of these sessions needs its token to be extended. We do this after issuing the request so as to + // reduce network contention. + HashSet sessions = new HashSet(); + for (Request request : requests) { + if (request.session != null) { + sessions.add(request.session); + } + } + for (Session session : sessions) { + session.extendAccessTokenIfNeeded(); + } + + return responses; + } + + /** + * Asynchronously executes requests that have already been serialized into an HttpURLConnection. No validation is + * done that the contents of the connection actually reflect the serialized requests, so it is the caller's + * responsibility to ensure that it will correctly generate the desired responses. This function will return + * immediately, and the requests will be processed on a separate thread. In order to process results of a request, + * or determine whether a request succeeded or failed, a callback must be specified (see the + * {@link #setCallback(Callback) setCallback} method). + *

+ * This should only be called from the UI thread. + * + * @param connection + * the HttpURLConnection that the requests were serialized into + * @param requests + * the requests represented by the HttpURLConnection + * @return a RequestAsyncTask that is executing the request + */ + public static RequestAsyncTask executeConnectionAsync(HttpURLConnection connection, RequestBatch requests) { + return executeConnectionAsync(null, connection, requests); + } + + /** + * Asynchronously executes requests that have already been serialized into an HttpURLConnection. No validation is + * done that the contents of the connection actually reflect the serialized requests, so it is the caller's + * responsibility to ensure that it will correctly generate the desired responses. This function will return + * immediately, and the requests will be processed on a separate thread. In order to process results of a request, + * or determine whether a request succeeded or failed, a callback must be specified (see the + * {@link #setCallback(Callback) setCallback} method) + *

+ * This should only be called from the UI thread. + * + * @param callbackHandler + * a Handler that will be used to post calls to the callback for each request; if null, a Handler will be + * instantiated on the calling thread + * @param connection + * the HttpURLConnection that the requests were serialized into + * @param requests + * the requests represented by the HttpURLConnection + * @return a RequestAsyncTask that is executing the request + */ + public static RequestAsyncTask executeConnectionAsync(Handler callbackHandler, HttpURLConnection connection, + RequestBatch requests) { + Validate.notNull(connection, "connection"); + + RequestAsyncTask asyncTask = new RequestAsyncTask(connection, requests); + requests.setCallbackHandler(callbackHandler); + asyncTask.executeOnSettingsExecutor(); + return asyncTask; + } + + /** + * Returns a string representation of this Request, useful for debugging. + * + * @return the debugging information + */ + @Override + public String toString() { + return new StringBuilder().append("{Request: ").append(" session: ").append(session).append(", graphPath: ") + .append(graphPath).append(", graphObject: ").append(graphObject).append(", restMethod: ") + .append(restMethod).append(", httpMethod: ").append(httpMethod).append(", parameters: ") + .append(parameters).append("}").toString(); + } + + static void runCallbacks(final RequestBatch requests, List responses) { + int numRequests = requests.size(); + + // Compile the list of callbacks to call and then run them either on this thread or via the Handler we received + final ArrayList> callbacks = new ArrayList>(); + for (int i = 0; i < numRequests; ++i) { + Request request = requests.get(i); + if (request.callback != null) { + callbacks.add(new Pair(request.callback, responses.get(i))); + } + } + + if (callbacks.size() > 0) { + Runnable runnable = new Runnable() { + public void run() { + for (Pair pair : callbacks) { + pair.first.onCompleted(pair.second); + } + + List batchCallbacks = requests.getCallbacks(); + for (RequestBatch.Callback batchCallback : batchCallbacks) { + batchCallback.onBatchCompleted(requests); + } + } + }; + + Handler callbackHandler = requests.getCallbackHandler(); + if (callbackHandler == null) { + // Run on this thread. + runnable.run(); + } else { + // Post to the handler. + callbackHandler.post(runnable); + } + } + } + + static HttpURLConnection createConnection(URL url) throws IOException { + HttpURLConnection connection; + connection = (HttpURLConnection) url.openConnection(); + + connection.setRequestProperty(USER_AGENT_HEADER, getUserAgent()); + connection.setRequestProperty(CONTENT_TYPE_HEADER, getMimeContentType()); + connection.setRequestProperty(ACCEPT_LANGUAGE_HEADER, Locale.getDefault().toString()); + + connection.setChunkedStreamingMode(0); + return connection; + } + + + private void addCommonParameters() { + if (this.session != null) { + if (!this.session.isOpened()) { + throw new FacebookException("Session provided to a Request in un-opened state."); + } else if (!this.parameters.containsKey(ACCESS_TOKEN_PARAM)) { + String accessToken = this.session.getAccessToken(); + Logger.registerAccessToken(accessToken); + this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken); + } + } else if (!this.parameters.containsKey(ACCESS_TOKEN_PARAM)) { + String appID = Settings.getApplicationId(); + String clientToken = Settings.getClientToken(); + if (!Utility.isNullOrEmpty(appID) && !Utility.isNullOrEmpty(clientToken)) { + String accessToken = appID + "|" + clientToken; + this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken); + } else { + Log.d(TAG, + "Warning: Sessionless Request needs token but missing either application ID or client token."); + } + } + this.parameters.putString(SDK_PARAM, SDK_ANDROID); + this.parameters.putString(FORMAT_PARAM, FORMAT_JSON); + } + + private String appendParametersToBaseUrl(String baseUrl) { + Uri.Builder uriBuilder = new Uri.Builder().encodedPath(baseUrl); + + Set keys = this.parameters.keySet(); + for (String key : keys) { + Object value = this.parameters.get(key); + + if (value == null) { + value = ""; + } + + if (isSupportedParameterType(value)) { + value = parameterToString(value); + } else { + if (httpMethod == HttpMethod.GET) { + throw new IllegalArgumentException(String.format("Unsupported parameter type for GET request: %s", + value.getClass().getSimpleName())); + } + continue; + } + + uriBuilder.appendQueryParameter(key, value.toString()); + } + + return uriBuilder.toString(); + } + + final String getUrlForBatchedRequest() { + if (overriddenURL != null) { + throw new FacebookException("Can't override URL for a batch request"); + } + + String baseUrl; + if (this.restMethod != null) { + baseUrl = getRestPathWithVersion(); + } else { + baseUrl = getGraphPathWithVersion(); + } + + addCommonParameters(); + return appendParametersToBaseUrl(baseUrl); + } + + final String getUrlForSingleRequest() { + if (overriddenURL != null) { + return overriddenURL.toString(); + } + + String baseUrl; + if (this.restMethod != null) { + baseUrl = String.format("%s/%s", ServerProtocol.getRestUrlBase(), getRestPathWithVersion()); + } else { + String graphBaseUrlBase; + if (this.getHttpMethod() == HttpMethod.POST && graphPath != null && graphPath.endsWith(VIDEOS_SUFFIX)) { + graphBaseUrlBase = ServerProtocol.getGraphVideoUrlBase(); + } else { + graphBaseUrlBase = ServerProtocol.getGraphUrlBase(); + } + baseUrl = String.format("%s/%s", graphBaseUrlBase, getGraphPathWithVersion()); + } + + addCommonParameters(); + return appendParametersToBaseUrl(baseUrl); + } + + private String getGraphPathWithVersion() { + Matcher matcher = versionPattern.matcher(this.graphPath); + if (matcher.matches()) { + return this.graphPath; + } + return String.format("%s/%s", this.version, this.graphPath); + } + + private String getRestPathWithVersion() { + Matcher matcher = versionPattern.matcher(this.restMethod); + if (matcher.matches()) { + return this.restMethod; + } + return String.format("%s/%s/%s", this.version, ServerProtocol.REST_METHOD_BASE, this.restMethod); + } + + private static class Attachment { + private final Request request; + private final Object value; + + public Attachment(Request request, Object value) { + this.request = request; + this.value = value; + } + + public Request getRequest() { + return request; + } + + public Object getValue() { + return value; + } + } + + private void serializeToBatch(JSONArray batch, Map attachments) throws JSONException, IOException { + JSONObject batchEntry = new JSONObject(); + + if (this.batchEntryName != null) { + batchEntry.put(BATCH_ENTRY_NAME_PARAM, this.batchEntryName); + batchEntry.put(BATCH_ENTRY_OMIT_RESPONSE_ON_SUCCESS_PARAM, this.batchEntryOmitResultOnSuccess); + } + if (this.batchEntryDependsOn != null) { + batchEntry.put(BATCH_ENTRY_DEPENDS_ON_PARAM, this.batchEntryDependsOn); + } + + String relativeURL = getUrlForBatchedRequest(); + batchEntry.put(BATCH_RELATIVE_URL_PARAM, relativeURL); + batchEntry.put(BATCH_METHOD_PARAM, httpMethod); + if (this.session != null) { + String accessToken = this.session.getAccessToken(); + Logger.registerAccessToken(accessToken); + } + + // Find all of our attachments. Remember their names and put them in the attachment map. + ArrayList attachmentNames = new ArrayList(); + Set keys = this.parameters.keySet(); + for (String key : keys) { + Object value = this.parameters.get(key); + if (isSupportedAttachmentType(value)) { + // Make the name unique across this entire batch. + String name = String.format("%s%d", ATTACHMENT_FILENAME_PREFIX, attachments.size()); + attachmentNames.add(name); + attachments.put(name, new Attachment(this, value)); + } + } + + if (!attachmentNames.isEmpty()) { + String attachmentNamesString = TextUtils.join(",", attachmentNames); + batchEntry.put(ATTACHED_FILES_PARAM, attachmentNamesString); + } + + if (this.graphObject != null) { + // Serialize the graph object into the "body" parameter. + final ArrayList keysAndValues = new ArrayList(); + processGraphObject(this.graphObject, relativeURL, new KeyValueSerializer() { + @Override + public void writeString(String key, String value) throws IOException { + keysAndValues.add(String.format("%s=%s", key, URLEncoder.encode(value, "UTF-8"))); + } + }); + String bodyValue = TextUtils.join("&", keysAndValues); + batchEntry.put(BATCH_BODY_PARAM, bodyValue); + } + + batch.put(batchEntry); + } + + private void validate() { + if (graphPath != null && restMethod != null) { + throw new IllegalArgumentException("Only one of a graph path or REST method may be specified per request."); + } + } + + private static boolean hasOnProgressCallbacks(RequestBatch requests) { + for (RequestBatch.Callback callback : requests.getCallbacks()) { + if (callback instanceof RequestBatch.OnProgressCallback) { + return true; + } + } + + for (Request request : requests) { + if (request.getCallback() instanceof OnProgressCallback) { + return true; + } + } + + return false; + } + + final static void serializeToUrlConnection(RequestBatch requests, HttpURLConnection connection) + throws IOException, JSONException { + Logger logger = new Logger(LoggingBehavior.REQUESTS, "Request"); + + int numRequests = requests.size(); + + HttpMethod connectionHttpMethod = (numRequests == 1) ? requests.get(0).httpMethod : HttpMethod.POST; + connection.setRequestMethod(connectionHttpMethod.name()); + + URL url = connection.getURL(); + logger.append("Request:\n"); + logger.appendKeyValue("Id", requests.getId()); + logger.appendKeyValue("URL", url); + logger.appendKeyValue("Method", connection.getRequestMethod()); + logger.appendKeyValue("User-Agent", connection.getRequestProperty("User-Agent")); + logger.appendKeyValue("Content-Type", connection.getRequestProperty("Content-Type")); + + connection.setConnectTimeout(requests.getTimeout()); + connection.setReadTimeout(requests.getTimeout()); + + // If we have a single non-POST request, don't try to serialize anything or HttpURLConnection will + // turn it into a POST. + boolean isPost = (connectionHttpMethod == HttpMethod.POST); + if (!isPost) { + logger.log(); + return; + } + + connection.setDoOutput(true); + + OutputStream outputStream = null; + try { + if (hasOnProgressCallbacks(requests)) { + ProgressNoopOutputStream countingStream = null; + countingStream = new ProgressNoopOutputStream(requests.getCallbackHandler()); + processRequest(requests, null, numRequests, url, countingStream); + + int max = countingStream.getMaxProgress(); + Map progressMap = countingStream.getProgressMap(); + + BufferedOutputStream buffered = new BufferedOutputStream(connection.getOutputStream()); + outputStream = new ProgressOutputStream(buffered, requests, progressMap, max); + } else { + outputStream = new BufferedOutputStream(connection.getOutputStream()); + } + + processRequest(requests, logger, numRequests, url, outputStream); + } + finally { + outputStream.close(); + } + + logger.log(); + } + + private static void processRequest(RequestBatch requests, Logger logger, int numRequests, URL url, OutputStream outputStream) + throws IOException, JSONException + { + Serializer serializer = new Serializer(outputStream, logger); + + if (numRequests == 1) { + Request request = requests.get(0); + + Map attachments = new HashMap(); + for(String key : request.parameters.keySet()) { + Object value = request.parameters.get(key); + if (isSupportedAttachmentType(value)) { + attachments.put(key, new Attachment(request, value)); + } + } + + if (logger != null) { + logger.append(" Parameters:\n"); + } + serializeParameters(request.parameters, serializer, request); + + if (logger != null) { + logger.append(" Attachments:\n"); + } + serializeAttachments(attachments, serializer); + + if (request.graphObject != null) { + processGraphObject(request.graphObject, url.getPath(), serializer); + } + } else { + String batchAppID = getBatchAppId(requests); + if (Utility.isNullOrEmpty(batchAppID)) { + throw new FacebookException("At least one request in a batch must have an open Session, or a " + + "default app ID must be specified."); + } + + serializer.writeString(BATCH_APP_ID_PARAM, batchAppID); + + // We write out all the requests as JSON, remembering which file attachments they have, then + // write out the attachments. + Map attachments = new HashMap(); + serializeRequestsAsJSON(serializer, requests, attachments); + + if (logger != null) { + logger.append(" Attachments:\n"); + } + serializeAttachments(attachments, serializer); + } + } + + private static void processGraphObject(GraphObject graphObject, String path, KeyValueSerializer serializer) + throws IOException { + // In general, graph objects are passed by reference (ID/URL). But if this is an OG Action, + // we need to pass the entire values of the contents of the 'image' property, as they + // contain important metadata beyond just a URL. We don't have a 100% foolproof way of knowing + // if we are posting an OG Action, given that batched requests can have parameter substitution, + // but passing the OG Action type as a substituted parameter is unlikely. + // It looks like an OG Action if it's posted to me/namespace:action[?other=stuff]. + boolean isOGAction = false; + if (path.startsWith("me/") || path.startsWith("/me/")) { + int colonLocation = path.indexOf(":"); + int questionMarkLocation = path.indexOf("?"); + isOGAction = colonLocation > 3 && (questionMarkLocation == -1 || colonLocation < questionMarkLocation); + } + + Set> entries = graphObject.asMap().entrySet(); + for (Entry entry : entries) { + boolean passByValue = isOGAction && entry.getKey().equalsIgnoreCase("image"); + processGraphObjectProperty(entry.getKey(), entry.getValue(), serializer, passByValue); + } + } + + private static void processGraphObjectProperty(String key, Object value, KeyValueSerializer serializer, + boolean passByValue) throws IOException { + Class valueClass = value.getClass(); + if (GraphObject.class.isAssignableFrom(valueClass)) { + value = ((GraphObject) value).getInnerJSONObject(); + valueClass = value.getClass(); + } else if (GraphObjectList.class.isAssignableFrom(valueClass)) { + value = ((GraphObjectList) value).getInnerJSONArray(); + valueClass = value.getClass(); + } + + if (JSONObject.class.isAssignableFrom(valueClass)) { + JSONObject jsonObject = (JSONObject) value; + if (passByValue) { + // We need to pass all properties of this object in key[propertyName] format. + @SuppressWarnings("unchecked") + Iterator keys = jsonObject.keys(); + while (keys.hasNext()) { + String propertyName = keys.next(); + String subKey = String.format("%s[%s]", key, propertyName); + processGraphObjectProperty(subKey, jsonObject.opt(propertyName), serializer, passByValue); + } + } else { + // Normal case is passing objects by reference, so just pass the ID or URL, if any, as the value + // for "key" + if (jsonObject.has("id")) { + processGraphObjectProperty(key, jsonObject.optString("id"), serializer, passByValue); + } else if (jsonObject.has("url")) { + processGraphObjectProperty(key, jsonObject.optString("url"), serializer, passByValue); + } + } + } else if (JSONArray.class.isAssignableFrom(valueClass)) { + JSONArray jsonArray = (JSONArray) value; + int length = jsonArray.length(); + for (int i = 0; i < length; ++i) { + String subKey = String.format("%s[%d]", key, i); + processGraphObjectProperty(subKey, jsonArray.opt(i), serializer, passByValue); + } + } else if (String.class.isAssignableFrom(valueClass) || + Number.class.isAssignableFrom(valueClass) || + Boolean.class.isAssignableFrom(valueClass)) { + serializer.writeString(key, value.toString()); + } else if (Date.class.isAssignableFrom(valueClass)) { + Date date = (Date) value; + // The "Events Timezone" platform migration affects what date/time formats Facebook accepts and returns. + // Apps created after 8/1/12 (or apps that have explicitly enabled the migration) should send/receive + // dates in ISO-8601 format. Pre-migration apps can send as Unix timestamps. Since the future is ISO-8601, + // that is what we support here. Apps that need pre-migration behavior can explicitly send these as + // integer timestamps rather than Dates. + final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US); + serializer.writeString(key, iso8601DateFormat.format(date)); + } + } + + private static void serializeParameters(Bundle bundle, Serializer serializer, Request request) throws IOException { + Set keys = bundle.keySet(); + + for (String key : keys) { + Object value = bundle.get(key); + if (isSupportedParameterType(value)) { + serializer.writeObject(key, value, request); + } + } + } + + private static void serializeAttachments(Map attachments, Serializer serializer) throws IOException { + Set keys = attachments.keySet(); + + for (String key : keys) { + Attachment attachment = attachments.get(key); + if (isSupportedAttachmentType(attachment.getValue())) { + serializer.writeObject(key, attachment.getValue(), attachment.getRequest()); + } + } + } + + private static void serializeRequestsAsJSON(Serializer serializer, Collection requests, Map attachments) + throws JSONException, IOException { + JSONArray batch = new JSONArray(); + for (Request request : requests) { + request.serializeToBatch(batch, attachments); + } + + serializer.writeRequestsAsJson(BATCH_PARAM, batch, requests); + } + + private static String getMimeContentType() { + return String.format("multipart/form-data; boundary=%s", MIME_BOUNDARY); + } + + private static volatile String userAgent; + + private static String getUserAgent() { + if (userAgent == null) { + userAgent = String.format("%s.%s", USER_AGENT_BASE, FacebookSdkVersion.BUILD); + } + + return userAgent; + } + + private static String getBatchAppId(RequestBatch batch) { + if (!Utility.isNullOrEmpty(batch.getBatchApplicationId())) { + return batch.getBatchApplicationId(); + } + + for (Request request : batch) { + Session session = request.session; + if (session != null) { + return session.getApplicationId(); + } + } + return Request.defaultBatchApplicationId; + } + + private static List typedListFromResponse(Response response, Class clazz) { + GraphMultiResult multiResult = response.getGraphObjectAs(GraphMultiResult.class); + if (multiResult == null) { + return null; + } + + GraphObjectList data = multiResult.getData(); + if (data == null) { + return null; + } + + return data.castToListOf(clazz); + } + + private static boolean isSupportedAttachmentType(Object value) { + return value instanceof Bitmap || value instanceof byte[] || value instanceof ParcelFileDescriptor || + value instanceof ParcelFileDescriptorWithMimeType; + } + + private static boolean isSupportedParameterType(Object value) { + return value instanceof String || value instanceof Boolean || value instanceof Number || + value instanceof Date; + } + + private static String parameterToString(Object value) { + if (value instanceof String) { + return (String) value; + } else if (value instanceof Boolean || value instanceof Number) { + return value.toString(); + } else if (value instanceof Date) { + final SimpleDateFormat iso8601DateFormat = new SimpleDateFormat(ISO_8601_FORMAT_STRING, Locale.US); + return iso8601DateFormat.format(value); + } + throw new IllegalArgumentException("Unsupported parameter type."); + } + + private interface KeyValueSerializer { + void writeString(String key, String value) throws IOException; + } + + private static class Serializer implements KeyValueSerializer { + private final OutputStream outputStream; + private final Logger logger; + private boolean firstWrite = true; + + public Serializer(OutputStream outputStream, Logger logger) { + this.outputStream = outputStream; + this.logger = logger; + } + + public void writeObject(String key, Object value, Request request) throws IOException { + if (outputStream instanceof RequestOutputStream) { + ((RequestOutputStream) outputStream).setCurrentRequest(request); + } + + if (isSupportedParameterType(value)) { + writeString(key, parameterToString(value)); + } else if (value instanceof Bitmap) { + writeBitmap(key, (Bitmap) value); + } else if (value instanceof byte[]) { + writeBytes(key, (byte[]) value); + } else if (value instanceof ParcelFileDescriptor) { + writeFile(key, (ParcelFileDescriptor) value, null); + } else if (value instanceof ParcelFileDescriptorWithMimeType) { + writeFile(key, (ParcelFileDescriptorWithMimeType) value); + } else { + throw new IllegalArgumentException("value is not a supported type: String, Bitmap, byte[]"); + } + } + + public void writeRequestsAsJson(String key, JSONArray requestJsonArray, Collection requests) + throws IOException, JSONException { + if (! (outputStream instanceof RequestOutputStream)) { + writeString(key, requestJsonArray.toString()); + return; + } + + RequestOutputStream requestOutputStream = (RequestOutputStream) outputStream; + writeContentDisposition(key, null, null); + write("["); + int i = 0; + for (Request request : requests) { + JSONObject requestJson = requestJsonArray.getJSONObject(i); + requestOutputStream.setCurrentRequest(request); + if (i > 0) { + write(",%s", requestJson.toString()); + } else { + write("%s", requestJson.toString()); + } + i++; + } + write("]"); + if (logger != null) { + logger.appendKeyValue(" " + key, requestJsonArray.toString()); + } + } + + public void writeString(String key, String value) throws IOException { + writeContentDisposition(key, null, null); + writeLine("%s", value); + writeRecordBoundary(); + if (logger != null) { + logger.appendKeyValue(" " + key, value); + } + } + + public void writeBitmap(String key, Bitmap bitmap) throws IOException { + writeContentDisposition(key, key, "image/png"); + // Note: quality parameter is ignored for PNG + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + writeLine(""); + writeRecordBoundary(); + if (logger != null) { + logger.appendKeyValue(" " + key, ""); + } + } + + public void writeBytes(String key, byte[] bytes) throws IOException { + writeContentDisposition(key, key, "content/unknown"); + this.outputStream.write(bytes); + writeLine(""); + writeRecordBoundary(); + if (logger != null) { + logger.appendKeyValue(" " + key, String.format("", bytes.length)); + } + } + + public void writeFile(String key, ParcelFileDescriptorWithMimeType descriptorWithMimeType) throws IOException { + writeFile(key, descriptorWithMimeType.getFileDescriptor(), descriptorWithMimeType.getMimeType()); + } + + public void writeFile(String key, ParcelFileDescriptor descriptor, String mimeType) throws IOException { + if (mimeType == null) { + mimeType = "content/unknown"; + } + writeContentDisposition(key, key, mimeType); + + int totalBytes = 0; + + if (outputStream instanceof ProgressNoopOutputStream) { + // If we are only counting bytes then skip reading the file + ((ProgressNoopOutputStream) outputStream).addProgress(descriptor.getStatSize()); + } else { + ParcelFileDescriptor.AutoCloseInputStream inputStream = null; + BufferedInputStream bufferedInputStream = null; + try { + inputStream = new ParcelFileDescriptor.AutoCloseInputStream(descriptor); + bufferedInputStream = new BufferedInputStream(inputStream); + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = bufferedInputStream.read(buffer)) != -1) { + this.outputStream.write(buffer, 0, bytesRead); + totalBytes += bytesRead; + } + } finally { + if (bufferedInputStream != null) { + bufferedInputStream.close(); + } + if (inputStream != null) { + inputStream.close(); + } + } + } + writeLine(""); + writeRecordBoundary(); + if (logger != null) { + logger.appendKeyValue(" " + key, String.format("", totalBytes)); + } + } + + public void writeRecordBoundary() throws IOException { + writeLine("--%s", MIME_BOUNDARY); + } + + public void writeContentDisposition(String name, String filename, String contentType) throws IOException { + write("Content-Disposition: form-data; name=\"%s\"", name); + if (filename != null) { + write("; filename=\"%s\"", filename); + } + writeLine(""); // newline after Content-Disposition + if (contentType != null) { + writeLine("%s: %s", CONTENT_TYPE_HEADER, contentType); + } + writeLine(""); // blank line before content + } + + public void write(String format, Object... args) throws IOException { + if (firstWrite) { + // Prepend all of our output with a boundary string. + this.outputStream.write("--".getBytes()); + this.outputStream.write(MIME_BOUNDARY.getBytes()); + this.outputStream.write("\r\n".getBytes()); + firstWrite = false; + } + this.outputStream.write(String.format(format, args).getBytes()); + } + + public void writeLine(String format, Object... args) throws IOException { + write(format, args); + write("\r\n"); + } + + } + + /** + * Specifies the interface that consumers of the Request class can implement in order to be notified when a + * particular request completes, either successfully or with an error. + */ + public interface Callback { + /** + * The method that will be called when a request completes. + * + * @param response + * the Response of this request, which may include error information if the request was unsuccessful + */ + void onCompleted(Response response); + } + + /** + * Specifies the interface that consumers of the Request class can implement in order to be notified when a + * progress is made on a particular request. The frequency of the callbacks can be controlled using + * {@link com.facebook.Settings#setOnProgressThreshold(long)} + */ + public interface OnProgressCallback extends Callback { + /** + * The method that will be called when progress is made. + * + * @param current + * the current value of the progress of the request. + * @param max + * the maximum value (target) value that the progress will have. + */ + void onProgress(long current, long max); + } + + /** + * Specifies the interface that consumers of + * {@link Request#executeMeRequestAsync(Session, com.facebook.Request.GraphUserCallback)} + * can use to be notified when the request completes, either successfully or with an error. + */ + public interface GraphUserCallback { + /** + * The method that will be called when the request completes. + * + * @param user the GraphObject representing the returned user, or null + * @param response the Response of this request, which may include error information if the request was unsuccessful + */ + void onCompleted(GraphUser user, Response response); + } + + /** + * Specifies the interface that consumers of + * {@link Request#executeMyFriendsRequestAsync(Session, com.facebook.Request.GraphUserListCallback)} + * can use to be notified when the request completes, either successfully or with an error. + */ + public interface GraphUserListCallback { + /** + * The method that will be called when the request completes. + * + * @param users the list of GraphObjects representing the returned friends, or null + * @param response the Response of this request, which may include error information if the request was unsuccessful + */ + void onCompleted(List users, Response response); + } + + /** + * Specifies the interface that consumers of + * {@link Request#executePlacesSearchRequestAsync(Session, android.location.Location, int, int, String, com.facebook.Request.GraphPlaceListCallback)} + * can use to be notified when the request completes, either successfully or with an error. + */ + public interface GraphPlaceListCallback { + /** + * The method that will be called when the request completes. + * + * @param places the list of GraphObjects representing the returned places, or null + * @param response the Response of this request, which may include error information if the request was unsuccessful + */ + void onCompleted(List places, Response response); + } + + private static class ParcelFileDescriptorWithMimeType implements Parcelable { + private final String mimeType; + private final ParcelFileDescriptor fileDescriptor; + + public String getMimeType() { + return mimeType; + } + + public ParcelFileDescriptor getFileDescriptor() { + return fileDescriptor; + } + + public int describeContents() { + return CONTENTS_FILE_DESCRIPTOR; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mimeType); + out.writeFileDescriptor(fileDescriptor.getFileDescriptor()); + } + + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public ParcelFileDescriptorWithMimeType createFromParcel(Parcel in) { + return new ParcelFileDescriptorWithMimeType(in); + } + + public ParcelFileDescriptorWithMimeType[] newArray(int size) { + return new ParcelFileDescriptorWithMimeType[size]; + } + }; + + public ParcelFileDescriptorWithMimeType(ParcelFileDescriptor fileDescriptor, String mimeType) { + this.mimeType = mimeType; + this.fileDescriptor = fileDescriptor; + } + + private ParcelFileDescriptorWithMimeType(Parcel in) { + mimeType = in.readString(); + fileDescriptor = in.readFileDescriptor(); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestAsyncTask.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestAsyncTask.java new file mode 100644 index 0000000000..db60a18f99 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestAsyncTask.java @@ -0,0 +1,189 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.annotation.TargetApi; +import android.os.AsyncTask; +import android.os.Handler; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Defines an AsyncTask suitable for executing a Request in the background. May be subclassed + * by applications having unique threading model needs. + */ +@TargetApi(3) +public class RequestAsyncTask extends AsyncTask> { + private static final String TAG = RequestAsyncTask.class.getCanonicalName(); + private static Method executeOnExecutorMethod; + + private final HttpURLConnection connection; + private final RequestBatch requests; + + private Exception exception; + + static { + for (Method method : AsyncTask.class.getMethods()) { + if ("executeOnExecutor".equals(method.getName())) { + Class[] parameters = method.getParameterTypes(); + if ((parameters.length == 2) && (parameters[0] == Executor.class) && parameters[1].isArray()) { + executeOnExecutorMethod = method; + break; + } + } + } + } + + /** + * Constructor. Serialization of the requests will be done in the background, so any serialization- + * related errors will be returned via the Response.getException() method. + * + * @param requests the requests to execute + */ + public RequestAsyncTask(Request... requests) { + this(null, new RequestBatch(requests)); + } + + /** + * Constructor. Serialization of the requests will be done in the background, so any serialization- + * related errors will be returned via the Response.getException() method. + * + * @param requests the requests to execute + */ + public RequestAsyncTask(Collection requests) { + this(null, new RequestBatch(requests)); + } + + /** + * Constructor. Serialization of the requests will be done in the background, so any serialization- + * related errors will be returned via the Response.getException() method. + * + * @param requests the requests to execute + */ + public RequestAsyncTask(RequestBatch requests) { + this(null, requests); + } + + /** + * Constructor that allows specification of an HTTP connection to use for executing + * the requests. No validation is done that the contents of the connection actually + * reflect the serialized requests, so it is the caller's responsibility to ensure + * that it will correctly generate the desired responses. + * + * @param connection the HTTP connection to use to execute the requests + * @param requests the requests to execute + */ + public RequestAsyncTask(HttpURLConnection connection, Request... requests) { + this(connection, new RequestBatch(requests)); + } + + /** + * Constructor that allows specification of an HTTP connection to use for executing + * the requests. No validation is done that the contents of the connection actually + * reflect the serialized requests, so it is the caller's responsibility to ensure + * that it will correctly generate the desired responses. + * + * @param connection the HTTP connection to use to execute the requests + * @param requests the requests to execute + */ + public RequestAsyncTask(HttpURLConnection connection, Collection requests) { + this(connection, new RequestBatch(requests)); + } + + /** + * Constructor that allows specification of an HTTP connection to use for executing + * the requests. No validation is done that the contents of the connection actually + * reflect the serialized requests, so it is the caller's responsibility to ensure + * that it will correctly generate the desired responses. + * + * @param connection the HTTP connection to use to execute the requests + * @param requests the requests to execute + */ + public RequestAsyncTask(HttpURLConnection connection, RequestBatch requests) { + this.requests = requests; + this.connection = connection; + } + + protected final Exception getException() { + return exception; + } + + protected final RequestBatch getRequests() { + return requests; + } + + @Override + public String toString() { + return new StringBuilder().append("{RequestAsyncTask: ").append(" connection: ").append(connection) + .append(", requests: ").append(requests).append("}").toString(); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + if (requests.getCallbackHandler() == null) { + // We want any callbacks to go to a handler on this thread unless a handler has already been specified. + requests.setCallbackHandler(new Handler()); + } + } + + @Override + protected void onPostExecute(List result) { + super.onPostExecute(result); + + if (exception != null) { + Log.d(TAG, String.format("onPostExecute: exception encountered during request: %s", exception.getMessage())); + } + } + + @Override + protected List doInBackground(Void... params) { + try { + if (connection == null) { + return requests.executeAndWait(); + } else { + return Request.executeConnectionAndWait(connection, requests); + } + } catch (Exception e) { + exception = e; + return null; + } + } + + RequestAsyncTask executeOnSettingsExecutor() { + try { + if (executeOnExecutorMethod != null) { + executeOnExecutorMethod.invoke(this, Settings.getExecutor(), null); + return this; + } + } catch (InvocationTargetException e) { + // fall-through + } catch (IllegalAccessException e) { + // fall-through + } + + this.execute(); + return this; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestBatch.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestBatch.java new file mode 100644 index 0000000000..9ecd7be18e --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestBatch.java @@ -0,0 +1,243 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Handler; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * RequestBatch contains a list of Request objects that can be sent to Facebook in a single round-trip. + */ +public class RequestBatch extends AbstractList { + private static AtomicInteger idGenerator = new AtomicInteger(); + + private Handler callbackHandler; + private List requests = new ArrayList(); + private int timeoutInMilliseconds = 0; + private final String id = Integer.valueOf(idGenerator.incrementAndGet()).toString(); + private List callbacks = new ArrayList(); + private String batchApplicationId; + + /** + * Constructor. Creates an empty batch. + */ + public RequestBatch() { + this.requests = new ArrayList(); + } + + /** + * Constructor. + * @param requests the requests to add to the batch + */ + public RequestBatch(Collection requests) { + this.requests = new ArrayList(requests); + } + + /** + * Constructor. + * @param requests the requests to add to the batch + */ + public RequestBatch(Request... requests) { + this.requests = Arrays.asList(requests); + } + + /** + * Constructor. + * @param requests the requests to add to the batch + */ + public RequestBatch(RequestBatch requests) { + this.requests = new ArrayList(requests); + this.callbackHandler = requests.callbackHandler; + this.timeoutInMilliseconds = requests.timeoutInMilliseconds; + this.callbacks = new ArrayList(requests.callbacks); + } + + /** + * Gets the timeout to wait for responses from the server before a timeout error occurs. + * @return the timeout, in milliseconds; 0 (the default) means do not timeout + */ + public int getTimeout() { + return timeoutInMilliseconds; + } + + /** + * Sets the timeout to wait for responses from the server before a timeout error occurs. + * @param timeoutInMilliseconds the timeout, in milliseconds; 0 means do not timeout + */ + public void setTimeout(int timeoutInMilliseconds) { + if (timeoutInMilliseconds < 0) { + throw new IllegalArgumentException("Argument timeoutInMilliseconds must be >= 0."); + } + this.timeoutInMilliseconds = timeoutInMilliseconds; + } + + /** + * Adds a batch-level callback which will be called when the entire batch has finished executing. + * + * @param callback the callback + */ + public void addCallback(Callback callback) { + if (!callbacks.contains(callback)) { + callbacks.add(callback); + } + } + + /** + * Removes a batch-level callback. + * + * @param callback the callback + */ + public void removeCallback(Callback callback) { + callbacks.remove(callback); + } + + @Override + public final boolean add(Request request) { + return requests.add(request); + } + + @Override + public final void add(int location, Request request) { + requests.add(location, request); + } + + @Override + public final void clear() { + requests.clear(); + } + + @Override + public final Request get(int i) { + return requests.get(i); + } + + @Override + public final Request remove(int location) { + return requests.remove(location); + } + + @Override + public final Request set(int location, Request request) { + return requests.set(location, request); + } + + @Override + public final int size() { + return requests.size(); + } + + final String getId() { + return id; + } + + final Handler getCallbackHandler() { + return callbackHandler; + } + + final void setCallbackHandler(Handler callbackHandler) { + this.callbackHandler = callbackHandler; + } + + final List getRequests() { + return requests; + } + + final List getCallbacks() { + return callbacks; + } + + final String getBatchApplicationId() { + return batchApplicationId; + } + + final void setBatchApplicationId(String batchApplicationId) { + this.batchApplicationId = batchApplicationId; + } + + /** + * Executes this batch on the current thread and returns the responses. + *

+ * This should only be used if you have transitioned off the UI thread. + * + * @return a list of Response objects representing the results of the requests; responses are returned in the same + * order as the requests were specified. + * + * @throws FacebookException + * If there was an error in the protocol used to communicate with the service + * @throws IllegalArgumentException if the passed in RequestBatch is empty + * @throws NullPointerException if the passed in RequestBatch or any of its contents are null + */ + public final List executeAndWait() { + return executeAndWaitImpl(); + } + + /** + * Executes this batch asynchronously. This function will return immediately, and the batch will + * be processed on a separate thread. In order to process results of a request, or determine + * whether a request succeeded or failed, a callback must be specified (see + * {@link Request#setCallback(com.facebook.Request.Callback)}) + *

+ * This should only be called from the UI thread. + * + * @return a RequestAsyncTask that is executing the request + * + * @throws IllegalArgumentException if this batch is empty + * @throws NullPointerException if any of the contents of this batch are null + */ + public final RequestAsyncTask executeAsync() { + return executeAsyncImpl(); + } + + /** + * Specifies the interface that consumers of the RequestBatch class can implement in order to be notified when the + * entire batch completes execution. It will be called after all per-Request callbacks are called. + */ + public interface Callback { + /** + * The method that will be called when a batch completes. + * + * @param batch the RequestBatch containing the Requests which were executed + */ + void onBatchCompleted(RequestBatch batch); + } + + /** + * Specifies the interface that consumers of the RequestBatch class can implement in order to be notified when the + * batch makes progress. The frequency of the callbacks can be controlled using + * {@link com.facebook.Settings#setOnProgressThreshold(long)}. + */ + public interface OnProgressCallback extends Callback { + /** + * The method that will be called when a batch makes progress. + * + * @param batch the RequestBatch containing the Requests which were executed + * @param current the current value of the progress + * @param max the max (target) value of the progress + */ + void onBatchProgress(RequestBatch batch, long current, long max); + } + + List executeAndWaitImpl() { + return Request.executeBatchAndWait(this); + } + + RequestAsyncTask executeAsyncImpl() { + return Request.executeBatchAsync(this); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestOutputStream.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestOutputStream.java new file mode 100644 index 0000000000..323b7c73d0 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestOutputStream.java @@ -0,0 +1,21 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +interface RequestOutputStream { + void setCurrentRequest(Request r); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestProgress.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestProgress.java new file mode 100644 index 0000000000..60b541f876 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/RequestProgress.java @@ -0,0 +1,77 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Handler; + +class RequestProgress { + private final Request request; + private final Handler callbackHandler; + private final long threshold; + + private long progress, lastReportedProgress, maxProgress; + + RequestProgress(Handler callbackHandler, Request request) { + this.request = request; + this.callbackHandler = callbackHandler; + + this.threshold = Settings.getOnProgressThreshold(); + } + + long getProgress() { + return progress; + } + + long getMaxProgress() { + return maxProgress; + } + + void addProgress(long size) { + progress += size; + + if (progress >= lastReportedProgress + threshold || progress >= maxProgress) { + reportProgress(); + } + } + + void addToMax(long size) { + maxProgress += size; + } + + void reportProgress() { + if (progress > lastReportedProgress) { + Request.Callback callback = request.getCallback(); + if (maxProgress > 0 && callback instanceof Request.OnProgressCallback) { + // Keep copies to avoid threading issues + final long currentCopy = progress; + final long maxProgressCopy = maxProgress; + final Request.OnProgressCallback callbackCopy = (Request.OnProgressCallback) callback; + if (callbackHandler == null) { + callbackCopy.onProgress(currentCopy, maxProgressCopy); + } else { + callbackHandler.post(new Runnable() { + @Override + public void run() { + callbackCopy.onProgress(currentCopy, maxProgressCopy); + } + }); + } + lastReportedProgress = progress; + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/Response.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Response.java new file mode 100644 index 0000000000..a5e408d881 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Response.java @@ -0,0 +1,474 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import com.facebook.internal.*; +import com.facebook.model.GraphObject; +import com.facebook.model.GraphObjectList; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * Encapsulates the response, successful or otherwise, of a call to the Facebook platform. + */ +public class Response { + private final HttpURLConnection connection; + private final GraphObject graphObject; + private final GraphObjectList graphObjectList; + private final boolean isFromCache; + private final FacebookRequestError error; + private final String rawResponse; + private final Request request; + + /** + * Property name of non-JSON results in the GraphObject. Certain calls to Facebook result in a non-JSON response + * (e.g., the string literal "true" or "false"). To present a consistent way of accessing results, these are + * represented as a GraphObject with a single string property with this name. + */ + public static final String NON_JSON_RESPONSE_PROPERTY = "FACEBOOK_NON_JSON_RESULT"; + + private static final int INVALID_SESSION_FACEBOOK_ERROR_CODE = 190; + + private static final String CODE_KEY = "code"; + private static final String BODY_KEY = "body"; + + private static final String RESPONSE_LOG_TAG = "Response"; + + private static final String RESPONSE_CACHE_TAG = "ResponseCache"; + private static FileLruCache responseCache; + + Response(Request request, HttpURLConnection connection, String rawResponse, GraphObject graphObject, boolean isFromCache) { + this(request, connection, rawResponse, graphObject, null, isFromCache, null); + } + + Response(Request request, HttpURLConnection connection, String rawResponse, GraphObjectList graphObjects, + boolean isFromCache) { + this(request, connection, rawResponse, null, graphObjects, isFromCache, null); + } + + Response(Request request, HttpURLConnection connection, FacebookRequestError error) { + this(request, connection, null, null, null, false, error); + } + + Response(Request request, HttpURLConnection connection, String rawResponse, GraphObject graphObject, GraphObjectList graphObjects, boolean isFromCache, FacebookRequestError error) { + this.request = request; + this.connection = connection; + this.rawResponse = rawResponse; + this.graphObject = graphObject; + this.graphObjectList = graphObjects; + this.isFromCache = isFromCache; + this.error = error; + } + + /** + * Returns information about any errors that may have occurred during the request. + * + * @return the error from the server, or null if there was no server error + */ + public final FacebookRequestError getError() { + return error; + } + + /** + * The single graph object returned for this request, if any. + * + * @return the graph object returned, or null if none was returned (or if the result was a list) + */ + public final GraphObject getGraphObject() { + return graphObject; + } + + /** + * The single graph object returned for this request, if any, cast into a particular type of GraphObject. + * + * @param graphObjectClass the GraphObject-derived interface to cast the graph object into + * @return the graph object returned, or null if none was returned (or if the result was a list) + * @throws FacebookException If the passed in Class is not a valid GraphObject interface + */ + public final T getGraphObjectAs(Class graphObjectClass) { + if (graphObject == null) { + return null; + } + if (graphObjectClass == null) { + throw new NullPointerException("Must pass in a valid interface that extends GraphObject"); + } + return graphObject.cast(graphObjectClass); + } + + /** + * The list of graph objects returned for this request, if any. + * + * @return the list of graph objects returned, or null if none was returned (or if the result was not a list) + */ + public final GraphObjectList getGraphObjectList() { + return graphObjectList; + } + + /** + * The list of graph objects returned for this request, if any, cast into a particular type of GraphObject. + * + * @param graphObjectClass the GraphObject-derived interface to cast the graph objects into + * @return the list of graph objects returned, or null if none was returned (or if the result was not a list) + * @throws FacebookException If the passed in Class is not a valid GraphObject interface + */ + public final GraphObjectList getGraphObjectListAs(Class graphObjectClass) { + if (graphObjectList == null) { + return null; + } + return graphObjectList.castToListOf(graphObjectClass); + } + + /** + * Returns the HttpURLConnection that this response was generated from. If the response was retrieved + * from the cache, this will be null. + * + * @return the connection, or null + */ + public final HttpURLConnection getConnection() { + return connection; + } + + /** + * Returns the request that this response is for. + * + * @return the request that this response is for + */ + public Request getRequest() { + return request; + } + + /** + * Returns the server response as a String that this response is for. + * + * @return A String representation of the actual response from the server + */ + public String getRawResponse() { + return rawResponse; + } + + /** + * Indicates whether paging is being done forward or backward. + */ + public enum PagingDirection { + /** + * Indicates that paging is being performed in the forward direction. + */ + NEXT, + /** + * Indicates that paging is being performed in the backward direction. + */ + PREVIOUS + } + + /** + * If a Response contains results that contain paging information, returns a new + * Request that will retrieve the next page of results, in whichever direction + * is desired. If no paging information is available, returns null. + * + * @param direction enum indicating whether to page forward or backward + * @return a Request that will retrieve the next page of results in the desired + * direction, or null if no paging information is available + */ + public Request getRequestForPagedResults(PagingDirection direction) { + String link = null; + if (graphObject != null) { + PagedResults pagedResults = graphObject.cast(PagedResults.class); + PagingInfo pagingInfo = pagedResults.getPaging(); + if (pagingInfo != null) { + if (direction == PagingDirection.NEXT) { + link = pagingInfo.getNext(); + } else { + link = pagingInfo.getPrevious(); + } + } + } + if (Utility.isNullOrEmpty(link)) { + return null; + } + + if (link != null && link.equals(request.getUrlForSingleRequest())) { + // We got the same "next" link as we just tried to retrieve. This could happen if cached + // data is invalid. All we can do in this case is pretend we have finished. + return null; + } + + Request pagingRequest; + try { + pagingRequest = new Request(request.getSession(), new URL(link)); + } catch (MalformedURLException e) { + return null; + } + + return pagingRequest; + } + + /** + * Provides a debugging string for this response. + */ + @Override + public String toString() { + String responseCode; + try { + responseCode = String.format("%d", (connection != null) ? connection.getResponseCode() : 200); + } catch (IOException e) { + responseCode = "unknown"; + } + + return new StringBuilder().append("{Response: ").append(" responseCode: ").append(responseCode) + .append(", graphObject: ").append(graphObject).append(", error: ").append(error) + .append(", isFromCache:").append(isFromCache).append("}") + .toString(); + } + + /** + * Indicates whether the response was retrieved from a local cache or from the server. + * + * @return true if the response was cached locally, false if it was retrieved from the server + */ + public final boolean getIsFromCache() { + return isFromCache; + } + + static FileLruCache getResponseCache() { + if (responseCache == null) { + Context applicationContext = Session.getStaticContext(); + if (applicationContext != null) { + responseCache = new FileLruCache(applicationContext, RESPONSE_CACHE_TAG, new FileLruCache.Limits()); + } + } + + return responseCache; + } + + @SuppressWarnings("resource") + static List fromHttpConnection(HttpURLConnection connection, RequestBatch requests) { + InputStream stream = null; + + FileLruCache cache = null; + String cacheKey = null; + if (requests instanceof CacheableRequestBatch) { + CacheableRequestBatch cacheableRequestBatch = (CacheableRequestBatch) requests; + cache = getResponseCache(); + cacheKey = cacheableRequestBatch.getCacheKeyOverride(); + if (Utility.isNullOrEmpty(cacheKey)) { + if (requests.size() == 1) { + // Default for single requests is to use the URL. + cacheKey = requests.get(0).getUrlForSingleRequest(); + } else { + Logger.log(LoggingBehavior.REQUESTS, RESPONSE_CACHE_TAG, + "Not using cache for cacheable request because no key was specified"); + } + } + + // Try loading from cache. If that fails, load from the network. + if (!cacheableRequestBatch.getForceRoundTrip() && cache != null && !Utility.isNullOrEmpty(cacheKey)) { + try { + stream = cache.get(cacheKey); + if (stream != null) { + return createResponsesFromStream(stream, null, requests, true); + } + } catch (FacebookException exception) { // retry via roundtrip below + } catch (JSONException exception) { + } catch (IOException exception) { + } finally { + Utility.closeQuietly(stream); + } + } + } + + // Load from the network, and cache the result if not an error. + try { + if (connection.getResponseCode() >= 400) { + stream = connection.getErrorStream(); + } else { + stream = connection.getInputStream(); + if ((cache != null) && (cacheKey != null) && (stream != null)) { + InputStream interceptStream = cache.interceptAndPut(cacheKey, stream); + if (interceptStream != null) { + stream = interceptStream; + } + } + } + + return createResponsesFromStream(stream, connection, requests, false); + } catch (FacebookException facebookException) { + Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response : %s", facebookException); + return constructErrorResponses(requests, connection, facebookException); + } catch (JSONException exception) { + Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response : %s", exception); + return constructErrorResponses(requests, connection, new FacebookException(exception)); + } catch (IOException exception) { + Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response : %s", exception); + return constructErrorResponses(requests, connection, new FacebookException(exception)); + } catch (SecurityException exception) { + Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response : %s", exception); + return constructErrorResponses(requests, connection, new FacebookException(exception)); + } finally { + Utility.closeQuietly(stream); + } + } + + static List createResponsesFromStream(InputStream stream, HttpURLConnection connection, + RequestBatch requests, boolean isFromCache) throws FacebookException, JSONException, IOException { + + String responseString = Utility.readStreamToString(stream); + Logger.log(LoggingBehavior.INCLUDE_RAW_RESPONSES, RESPONSE_LOG_TAG, + "Response (raw)\n Size: %d\n Response:\n%s\n", responseString.length(), + responseString); + + return createResponsesFromString(responseString, connection, requests, isFromCache); + } + + static List createResponsesFromString(String responseString, HttpURLConnection connection, + RequestBatch requests, boolean isFromCache) throws FacebookException, JSONException, IOException { + JSONTokener tokener = new JSONTokener(responseString); + Object resultObject = tokener.nextValue(); + + List responses = createResponsesFromObject(connection, requests, resultObject, isFromCache); + Logger.log(LoggingBehavior.REQUESTS, RESPONSE_LOG_TAG, "Response\n Id: %s\n Size: %d\n Responses:\n%s\n", + requests.getId(), responseString.length(), responses); + + return responses; + } + + private static List createResponsesFromObject(HttpURLConnection connection, List requests, + Object object, boolean isFromCache) throws FacebookException, JSONException { + assert (connection != null) || isFromCache; + + int numRequests = requests.size(); + List responses = new ArrayList(numRequests); + Object originalResult = object; + + if (numRequests == 1) { + Request request = requests.get(0); + try { + // Single request case -- the entire response is the result, wrap it as "body" so we can handle it + // the same as we do in the batched case. We get the response code from the actual HTTP response, + // as opposed to the batched case where it is returned as a "code" element. + JSONObject jsonObject = new JSONObject(); + jsonObject.put(BODY_KEY, object); + int responseCode = (connection != null) ? connection.getResponseCode() : 200; + jsonObject.put(CODE_KEY, responseCode); + + JSONArray jsonArray = new JSONArray(); + jsonArray.put(jsonObject); + + // Pretend we got an array of 1 back. + object = jsonArray; + } catch (JSONException e) { + responses.add(new Response(request, connection, new FacebookRequestError(connection, e))); + } catch (IOException e) { + responses.add(new Response(request, connection, new FacebookRequestError(connection, e))); + } + } + + if (!(object instanceof JSONArray) || ((JSONArray) object).length() != numRequests) { + FacebookException exception = new FacebookException("Unexpected number of results"); + throw exception; + } + + JSONArray jsonArray = (JSONArray) object; + + for (int i = 0; i < jsonArray.length(); ++i) { + Request request = requests.get(i); + try { + Object obj = jsonArray.get(i); + responses.add(createResponseFromObject(request, connection, obj, isFromCache, originalResult)); + } catch (JSONException e) { + responses.add(new Response(request, connection, new FacebookRequestError(connection, e))); + } catch (FacebookException e) { + responses.add(new Response(request, connection, new FacebookRequestError(connection, e))); + } + } + + return responses; + } + + private static Response createResponseFromObject(Request request, HttpURLConnection connection, Object object, + boolean isFromCache, Object originalResult) throws JSONException { + if (object instanceof JSONObject) { + JSONObject jsonObject = (JSONObject) object; + + FacebookRequestError error = + FacebookRequestError.checkResponseAndCreateError(jsonObject, originalResult, connection); + if (error != null) { + if (error.getErrorCode() == INVALID_SESSION_FACEBOOK_ERROR_CODE) { + Session session = request.getSession(); + if (session != null) { + session.closeAndClearTokenInformation(); + } + } + return new Response(request, connection, error); + } + + Object body = Utility.getStringPropertyAsJSON(jsonObject, BODY_KEY, NON_JSON_RESPONSE_PROPERTY); + + if (body instanceof JSONObject) { + GraphObject graphObject = GraphObject.Factory.create((JSONObject) body); + return new Response(request, connection, body.toString(), graphObject, isFromCache); + } else if (body instanceof JSONArray) { + GraphObjectList graphObjectList = GraphObject.Factory.createList( + (JSONArray) body, GraphObject.class); + return new Response(request, connection, body.toString(), graphObjectList, isFromCache); + } + // We didn't get a body we understand how to handle, so pretend we got nothing. + object = JSONObject.NULL; + } + + if (object == JSONObject.NULL) { + return new Response(request, connection, object.toString(), (GraphObject)null, isFromCache); + } else { + throw new FacebookException("Got unexpected object type in response, class: " + + object.getClass().getSimpleName()); + } + } + + static List constructErrorResponses(List requests, HttpURLConnection connection, + FacebookException error) { + int count = requests.size(); + List responses = new ArrayList(count); + for (int i = 0; i < count; ++i) { + Response response = new Response(requests.get(i), connection, new FacebookRequestError(connection, error)); + responses.add(response); + } + return responses; + } + + interface PagingInfo extends GraphObject { + String getNext(); + + String getPrevious(); + } + + interface PagedResults extends GraphObject { + GraphObjectList getData(); + + PagingInfo getPaging(); + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/Session.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Session.java new file mode 100644 index 0000000000..d0d2b56569 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Session.java @@ -0,0 +1,2311 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.app.Activity; +import android.content.*; +import android.content.pm.ResolveInfo; +import android.os.*; +import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; +import android.text.TextUtils; +import android.util.Log; +import com.facebook.internal.*; +import com.facebook.model.GraphMultiResult; +import com.facebook.model.GraphObject; +import com.facebook.model.GraphObjectList; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.*; +import java.lang.ref.WeakReference; +import java.util.*; + +/** + *

+ * Session is used to authenticate a user and manage the user's session with + * Facebook. + *

+ *

+ * Sessions must be opened before they can be used to make a Request. When a + * Session is created, it attempts to initialize itself from a TokenCachingStrategy. + * Closing the session can optionally clear this cache. The Session lifecycle + * uses {@link SessionState SessionState} to indicate its state. Once a Session has + * been closed, it can't be re-opened; a new Session must be created. + *

+ *

+ * Instances of Session provide state change notification via a callback + * interface, {@link Session.StatusCallback StatusCallback}. + *

+ */ +public class Session implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The logging tag used by Session. + */ + public static final String TAG = Session.class.getCanonicalName(); + + /** + * The default activity code used for authorization. + * + * @see #openForRead(OpenRequest) + * open + */ + public static final int DEFAULT_AUTHORIZE_ACTIVITY_CODE = 0xface; + + /** + * If Session authorization fails and provides a web view error code, the + * web view error code is stored in the Bundle returned from + * {@link #getAuthorizationBundle getAuthorizationBundle} under this key. + */ + public static final String WEB_VIEW_ERROR_CODE_KEY = "com.facebook.sdk.WebViewErrorCode"; + + /** + * If Session authorization fails and provides a failing url, the failing + * url is stored in the Bundle returned from {@link #getAuthorizationBundle + * getAuthorizationBundle} under this key. + */ + public static final String WEB_VIEW_FAILING_URL_KEY = "com.facebook.sdk.FailingUrl"; + + /** + * The action used to indicate that the active session has been set. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_SET = "com.facebook.sdk.ACTIVE_SESSION_SET"; + + /** + * The action used to indicate that the active session has been set to null. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_UNSET = "com.facebook.sdk.ACTIVE_SESSION_UNSET"; + + /** + * The action used to indicate that the active session has been opened. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_OPENED = "com.facebook.sdk.ACTIVE_SESSION_OPENED"; + + /** + * The action used to indicate that the active session has been closed. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_CLOSED = "com.facebook.sdk.ACTIVE_SESSION_CLOSED"; + + private static final Object STATIC_LOCK = new Object(); + private static Session activeSession; + private static volatile Context staticContext; + + // Token extension constants + private static final int TOKEN_EXTEND_THRESHOLD_SECONDS = 24 * 60 * 60; // 1 + // day + private static final int TOKEN_EXTEND_RETRY_SECONDS = 60 * 60; // 1 hour + + private static final String SESSION_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.saveSessionKey"; + private static final String AUTH_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.authBundleKey"; + private static final String PUBLISH_PERMISSION_PREFIX = "publish"; + private static final String MANAGE_PERMISSION_PREFIX = "manage"; + + private static final String BASIC_INFO_PERMISSION = "basic_info"; + + @SuppressWarnings("serial") + private static final Set OTHER_PUBLISH_PERMISSIONS = new HashSet() {{ + add("ads_management"); + add("create_event"); + add("rsvp_event"); + }}; + + private String applicationId; + private SessionState state; + private AccessToken tokenInfo; + private Date lastAttemptedTokenExtendDate = new Date(0); + + private AuthorizationRequest pendingAuthorizationRequest; + private AuthorizationClient authorizationClient; + + // The following are not serialized with the Session object + private volatile Bundle authorizationBundle; + private final List callbacks; + private Handler handler; + private AutoPublishAsyncTask autoPublishAsyncTask; + // This is the object that synchronizes access to state and tokenInfo + private final Object lock = new Object(); + private TokenCachingStrategy tokenCachingStrategy; + private volatile TokenRefreshRequest currentTokenRefreshRequest; + private AppEventsLogger appEventsLogger; + + /** + * Serialization proxy for the Session class. This is version 1 of + * serialization. Future serializations may differ in format. This + * class should not be modified. If serializations formats change, + * create a new class SerializationProxyVx. + */ + private static class SerializationProxyV1 implements Serializable { + private static final long serialVersionUID = 7663436173185080063L; + private final String applicationId; + private final SessionState state; + private final AccessToken tokenInfo; + private final Date lastAttemptedTokenExtendDate; + private final boolean shouldAutoPublish; + private final AuthorizationRequest pendingAuthorizationRequest; + + SerializationProxyV1(String applicationId, SessionState state, + AccessToken tokenInfo, Date lastAttemptedTokenExtendDate, + boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest) { + this.applicationId = applicationId; + this.state = state; + this.tokenInfo = tokenInfo; + this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; + this.shouldAutoPublish = shouldAutoPublish; + this.pendingAuthorizationRequest = pendingAuthorizationRequest; + } + + private Object readResolve() { + return new Session(applicationId, state, tokenInfo, + lastAttemptedTokenExtendDate, shouldAutoPublish, pendingAuthorizationRequest); + } + } + + /** + * Serialization proxy for the Session class. This is version 2 of + * serialization. Future serializations may differ in format. This + * class should not be modified. If serializations formats change, + * create a new class SerializationProxyVx. + */ + private static class SerializationProxyV2 implements Serializable { + private static final long serialVersionUID = 7663436173185080064L; + private final String applicationId; + private final SessionState state; + private final AccessToken tokenInfo; + private final Date lastAttemptedTokenExtendDate; + private final boolean shouldAutoPublish; + private final AuthorizationRequest pendingAuthorizationRequest; + private final Set requestedPermissions; + + SerializationProxyV2(String applicationId, SessionState state, + AccessToken tokenInfo, Date lastAttemptedTokenExtendDate, + boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest, + Set requestedPermissions) { + this.applicationId = applicationId; + this.state = state; + this.tokenInfo = tokenInfo; + this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; + this.shouldAutoPublish = shouldAutoPublish; + this.pendingAuthorizationRequest = pendingAuthorizationRequest; + this.requestedPermissions = requestedPermissions; + } + + private Object readResolve() { + return new Session(applicationId, state, tokenInfo, + lastAttemptedTokenExtendDate, shouldAutoPublish, pendingAuthorizationRequest, requestedPermissions); + } + } + + /** + * Used by version 1 of the serialization proxy, do not modify. + */ + private Session(String applicationId, SessionState state, + AccessToken tokenInfo, Date lastAttemptedTokenExtendDate, + boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest) { + this.applicationId = applicationId; + this.state = state; + this.tokenInfo = tokenInfo; + this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; + this.pendingAuthorizationRequest = pendingAuthorizationRequest; + handler = new Handler(Looper.getMainLooper()); + currentTokenRefreshRequest = null; + tokenCachingStrategy = null; + callbacks = new ArrayList(); + } + + /** + * Used by version 2 of the serialization proxy, do not modify. + */ + private Session(String applicationId, SessionState state, + AccessToken tokenInfo, Date lastAttemptedTokenExtendDate, + boolean shouldAutoPublish, AuthorizationRequest pendingAuthorizationRequest, + Set requestedPermissions) { + this.applicationId = applicationId; + this.state = state; + this.tokenInfo = tokenInfo; + this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; + this.pendingAuthorizationRequest = pendingAuthorizationRequest; + handler = new Handler(Looper.getMainLooper()); + currentTokenRefreshRequest = null; + tokenCachingStrategy = null; + callbacks = new ArrayList(); + } + + /** + * Initializes a new Session with the specified context. + * + * @param currentContext The Activity or Service creating this Session. + */ + public Session(Context currentContext) { + this(currentContext, null, null, true); + } + + Session(Context context, String applicationId, TokenCachingStrategy tokenCachingStrategy) { + this(context, applicationId, tokenCachingStrategy, true); + } + + Session(Context context, String applicationId, TokenCachingStrategy tokenCachingStrategy, + boolean loadTokenFromCache) { + // if the application ID passed in is null, try to get it from the + // meta-data in the manifest. + if ((context != null) && (applicationId == null)) { + applicationId = Utility.getMetadataApplicationId(context); + } + + Validate.notNull(applicationId, "applicationId"); + + initializeStaticContext(context); + + if (tokenCachingStrategy == null) { + tokenCachingStrategy = new SharedPreferencesTokenCachingStrategy(staticContext); + } + + this.applicationId = applicationId; + this.tokenCachingStrategy = tokenCachingStrategy; + this.state = SessionState.CREATED; + this.pendingAuthorizationRequest = null; + this.callbacks = new ArrayList(); + this.handler = new Handler(Looper.getMainLooper()); + + Bundle tokenState = loadTokenFromCache ? tokenCachingStrategy.load() : null; + if (TokenCachingStrategy.hasTokenInformation(tokenState)) { + Date cachedExpirationDate = TokenCachingStrategy + .getDate(tokenState, TokenCachingStrategy.EXPIRATION_DATE_KEY); + Date now = new Date(); + + if ((cachedExpirationDate == null) || cachedExpirationDate.before(now)) { + // If expired or we require new permissions, clear out the + // current token cache. + tokenCachingStrategy.clear(); + this.tokenInfo = AccessToken.createEmptyToken(); + } else { + // Otherwise we have a valid token, so use it. + this.tokenInfo = AccessToken.createFromCache(tokenState); + this.state = SessionState.CREATED_TOKEN_LOADED; + } + } else { + this.tokenInfo = AccessToken.createEmptyToken(); + } + } + + /** + * Returns a Bundle containing data that was returned from Facebook during + * authorization. + * + * @return a Bundle containing data that was returned from Facebook during + * authorization. + */ + public final Bundle getAuthorizationBundle() { + synchronized (this.lock) { + return this.authorizationBundle; + } + } + + /** + * Returns a boolean indicating whether the session is opened. + * + * @return a boolean indicating whether the session is opened. + */ + public final boolean isOpened() { + synchronized (this.lock) { + return this.state.isOpened(); + } + } + + public final boolean isClosed() { + synchronized (this.lock) { + return this.state.isClosed(); + } + } + + /** + * Returns the current state of the Session. + * See {@link SessionState} for details. + * + * @return the current state of the Session. + */ + public final SessionState getState() { + synchronized (this.lock) { + return this.state; + } + } + + /** + * Returns the application id associated with this Session. + * + * @return the application id associated with this Session. + */ + public final String getApplicationId() { + return this.applicationId; + } + + /** + * Returns the access token String. + * + * @return the access token String, or null if there is no access token + */ + public final String getAccessToken() { + synchronized (this.lock) { + return (this.tokenInfo == null) ? null : this.tokenInfo.getToken(); + } + } + + /** + *

+ * Returns the Date at which the current token will expire. + *

+ *

+ * Note that Session automatically attempts to extend the lifetime of Tokens + * as needed when Facebook requests are made. + *

+ * + * @return the Date at which the current token will expire, or null if there is no access token + */ + public final Date getExpirationDate() { + synchronized (this.lock) { + return (this.tokenInfo == null) ? null : this.tokenInfo.getExpires(); + } + } + + /** + *

+ * Returns the list of permissions associated with the session. + *

+ *

+ * If there is a valid token, this represents the permissions granted by + * that token. This can change during calls to + * {@link #requestNewReadPermissions} + * or {@link #requestNewPublishPermissions}. + *

+ * + * @return the list of permissions associated with the session, or null if there is no access token + */ + public final List getPermissions() { + synchronized (this.lock) { + return (this.tokenInfo == null) ? null : this.tokenInfo.getPermissions(); + } + } + + /** + *

+ * Returns whether a particular permission has been granted + *

+ * + * @param permission The permission to check for + * @return true if the permission is granted, false otherwise + */ + public boolean isPermissionGranted(String permission) { + List grantedPermissions = getPermissions(); + if (grantedPermissions != null) { + return grantedPermissions.contains(permission); + } + return false; + } + + /** + *

+ * Returns the list of permissions that have been requested in this session but not granted + *

+ * + * @return the list of requested permissions that have been declined + */ + public final List getDeclinedPermissions() { + synchronized (this.lock) { + return (this.tokenInfo == null) ? null : this.tokenInfo.getDeclinedPermissions(); + } + } + + /** + *

+ * Logs a user in to Facebook. + *

+ *

+ * A session may not be used with {@link Request Request} and other classes + * in the SDK until it is open. If, prior to calling open, the session is in + * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} + * state, and the requested permissions are a subset of the previously authorized + * permissions, then the Session becomes usable immediately with no user interaction. + *

+ *

+ * The permissions associated with the openRequest passed to this method must + * be read permissions only (or null/empty). It is not allowed to pass publish + * permissions to this method and will result in an exception being thrown. + *

+ *

+ * Any open method must be called at most once, and cannot be called after the + * Session is closed. Calling the method at an invalid time will result in + * UnsuportedOperationException. + *

+ * + * @param openRequest the open request, can be null only if the Session is in the + * {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state + * @throws FacebookException if any publish or manage permissions are requested + */ + public final void openForRead(OpenRequest openRequest) { + open(openRequest, SessionAuthorizationType.READ); + } + + /** + *

+ * Logs a user in to Facebook. + *

+ *

+ * A session may not be used with {@link Request Request} and other classes + * in the SDK until it is open. If, prior to calling open, the session is in + * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} + * state, and the requested permissions are a subset of the previously authorized + * permissions, then the Session becomes usable immediately with no user interaction. + *

+ *

+ * The permissions associated with the openRequest passed to this method must + * be publish or manage permissions only and must be non-empty. Any read permissions + * will result in a warning, and may fail during server-side authorization. Also, an application + * must have at least basic read permissions prior to requesting publish permissions, so + * this method should only be used if the application knows that the user has already granted + * read permissions to the application; otherwise, openForRead should be used, followed by a + * call to requestNewPublishPermissions. For more information on this flow, see + * https://developers.facebook.com/docs/facebook-login/permissions/. + *

+ *

+ * Any open method must be called at most once, and cannot be called after the + * Session is closed. Calling the method at an invalid time will result in + * UnsuportedOperationException. + *

+ * + * @param openRequest the open request, can be null only if the Session is in the + * {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state + * @throws FacebookException if the passed in request is null or has no permissions set. + */ + public final void openForPublish(OpenRequest openRequest) { + open(openRequest, SessionAuthorizationType.PUBLISH); + } + + /** + * Opens a session based on an existing Facebook access token. This method should be used + * only in instances where an application has previously obtained an access token and wishes + * to import it into the Session/TokenCachingStrategy-based session-management system. An + * example would be an application which previously did not use the Facebook SDK for Android + * and implemented its own session-management scheme, but wishes to implement an upgrade path + * for existing users so they do not need to log in again when upgrading to a version of + * the app that uses the SDK. + *

+ * No validation is done that the token, token source, or permissions are actually valid. + * It is the caller's responsibility to ensure that these accurately reflect the state of + * the token that has been passed in, or calls to the Facebook API may fail. + * + * @param accessToken the access token obtained from Facebook + * @param callback a callback that will be called when the session status changes; may be null + */ + public final void open(AccessToken accessToken, StatusCallback callback) { + synchronized (this.lock) { + if (pendingAuthorizationRequest != null) { + throw new UnsupportedOperationException( + "Session: an attempt was made to open a session that has a pending request."); + } + + if (state.isClosed()) { + throw new UnsupportedOperationException( + "Session: an attempt was made to open a previously-closed session."); + } else if (state != SessionState.CREATED && state != SessionState.CREATED_TOKEN_LOADED) { + throw new UnsupportedOperationException( + "Session: an attempt was made to open an already opened session."); + } + + if (callback != null) { + addCallback(callback); + } + + this.tokenInfo = accessToken; + + if (this.tokenCachingStrategy != null) { + this.tokenCachingStrategy.save(accessToken.toCacheBundle()); + } + + final SessionState oldState = state; + state = SessionState.OPENED; + this.postStateChange(oldState, state, null); + } + + autoPublishAsync(); + } + + /** + *

+ * Issues a request to add new read permissions to the Session. + *

+ *

+ * If successful, this will update the set of permissions on this session to + * match the newPermissions. If this fails, the Session remains unchanged. + *

+ *

+ * The permissions associated with the newPermissionsRequest passed to this method must + * be read permissions only (or null/empty). It is not allowed to pass publish + * permissions to this method and will result in an exception being thrown. + *

+ * + * @param newPermissionsRequest the new permissions request + */ + public final void requestNewReadPermissions(NewPermissionsRequest newPermissionsRequest) { + requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.READ); + } + + /** + *

+ * Issues a request to add new publish or manage permissions to the Session. + *

+ *

+ * If successful, this will update the set of permissions on this session to + * match the newPermissions. If this fails, the Session remains unchanged. + *

+ *

+ * The permissions associated with the newPermissionsRequest passed to this method must + * be publish or manage permissions only and must be non-empty. Any read permissions + * will result in a warning, and may fail during server-side authorization. + *

+ * + * @param newPermissionsRequest the new permissions request + */ + public final void requestNewPublishPermissions(NewPermissionsRequest newPermissionsRequest) { + requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.PUBLISH); + } + + /** + *

+ * Issues a request to refresh the permissions on the session. + *

+ *

+ * If successful, this will update the permissions and call the app back with + * {@link SessionState#OPENED_TOKEN_UPDATED}. The session can then be queried to see the granted and declined + * permissions. If this fails because the user has removed the app, the session will close. + *

+ */ + public final void refreshPermissions() { + Request request = new Request(this, "me/permissions"); + request.setCallback(new Request.Callback() { + @Override + public void onCompleted(Response response) { + PermissionsPair permissionsPair = handlePermissionResponse(response); + if (permissionsPair != null) { + // Update our token with the refreshed permissions + synchronized (lock) { + tokenInfo = AccessToken.createFromTokenWithRefreshedPermissions(tokenInfo, + permissionsPair.getGrantedPermissions(), permissionsPair.getDeclinedPermissions()); + postStateChange(state, SessionState.OPENED_TOKEN_UPDATED, null); + } + } + } + }); + request.executeAsync(); + } + + /** + * Internal helper class that is used to hold two different permission lists (granted and declined) + */ + static class PermissionsPair { + List grantedPermissions; + List declinedPermissions; + + public PermissionsPair(List grantedPermissions, List declinedPermissions) { + this.grantedPermissions = grantedPermissions; + this.declinedPermissions = declinedPermissions; + } + + public List getGrantedPermissions() { + return grantedPermissions; + } + + public List getDeclinedPermissions() { + return declinedPermissions; + } + } + /** + * This parses a server response to a call to me/permissions. It will return the list of granted permissions. + * It will optionally update a session with the requested permissions. It also handles the distinction between + * 1.0 and 2.0 calls to the endpoint. + * + * @param response The server response + * @return A list of granted permissions or null if an error + */ + static PermissionsPair handlePermissionResponse(Response response) { + if (response.getError() != null) { + return null; + } + + GraphMultiResult result = response.getGraphObjectAs(GraphMultiResult.class); + if (result == null) { + return null; + } + + GraphObjectList data = result.getData(); + if (data == null || data.size() == 0) { + return null; + } + List grantedPermissions = new ArrayList(data.size()); + List declinedPermissions = new ArrayList(data.size()); + + // Check if we are dealing with v2.0 or v1.0 behavior until the server is updated + GraphObject firstObject = data.get(0); + if (firstObject.getProperty("permission") != null) { // v2.0 + for (GraphObject graphObject : data) { + String permission = (String) graphObject.getProperty("permission"); + if (permission.equals("installed")) { + continue; + } + String status = (String) graphObject.getProperty("status"); + if(status.equals("granted")) { + grantedPermissions.add(permission); + } else if (status.equals("declined")) { + declinedPermissions.add(permission); + } + } + } else { // v1.0 + Map permissionsMap = firstObject.asMap(); + for (Map.Entry entry : permissionsMap.entrySet()) { + if (entry.getKey().equals("installed")) { + continue; + } + if ((Integer)entry.getValue() == 1) { + grantedPermissions.add(entry.getKey()); + } + } + } + + return new PermissionsPair(grantedPermissions, declinedPermissions); + } + + /** + * Provides an implementation for {@link Activity#onActivityResult + * onActivityResult} that updates the Session based on information returned + * during the authorization flow. The Activity that calls open or + * requestNewPermissions should forward the resulting onActivityResult call here to + * update the Session state based on the contents of the resultCode and + * data. + * + * @param currentActivity The Activity that is forwarding the onActivityResult call. + * @param requestCode The requestCode parameter from the forwarded call. When this + * onActivityResult occurs as part of Facebook authorization + * flow, this value is the activityCode passed to open or + * authorize. + * @param resultCode An int containing the resultCode parameter from the forwarded + * call. + * @param data The Intent passed as the data parameter from the forwarded + * call. + * @return A boolean indicating whether the requestCode matched a pending + * authorization request for this Session. + */ + public final boolean onActivityResult(Activity currentActivity, int requestCode, int resultCode, Intent data) { + Validate.notNull(currentActivity, "currentActivity"); + + initializeStaticContext(currentActivity); + + synchronized (lock) { + if (pendingAuthorizationRequest == null || (requestCode != pendingAuthorizationRequest.getRequestCode())) { + return false; + } + } + + Exception exception = null; + AuthorizationClient.Result.Code code = AuthorizationClient.Result.Code.ERROR; + + if (data != null) { + AuthorizationClient.Result result = (AuthorizationClient.Result) data.getSerializableExtra( + LoginActivity.RESULT_KEY); + if (result != null) { + // This came from LoginActivity. + handleAuthorizationResult(resultCode, result); + return true; + } else if (authorizationClient != null) { + // Delegate to the auth client. + authorizationClient.onActivityResult(requestCode, resultCode, data); + return true; + } + } else if (resultCode == Activity.RESULT_CANCELED) { + exception = new FacebookOperationCanceledException("User canceled operation."); + code = AuthorizationClient.Result.Code.CANCEL; + } + + if (exception == null) { + exception = new FacebookException("Unexpected call to Session.onActivityResult"); + } + + logAuthorizationComplete(code, null, exception); + finishAuthOrReauth(null, exception); + + return true; + } + + /** + * Closes the local in-memory Session object, but does not clear the + * persisted token cache. + */ + public final void close() { + synchronized (this.lock) { + final SessionState oldState = this.state; + + switch (this.state) { + case CREATED: + case OPENING: + this.state = SessionState.CLOSED_LOGIN_FAILED; + postStateChange(oldState, this.state, new FacebookException( + "Log in attempt aborted.")); + break; + + case CREATED_TOKEN_LOADED: + case OPENED: + case OPENED_TOKEN_UPDATED: + this.state = SessionState.CLOSED; + postStateChange(oldState, this.state, null); + break; + + case CLOSED: + case CLOSED_LOGIN_FAILED: + break; + } + } + } + + /** + * Closes the local in-memory Session object and clears any persisted token + * cache related to the Session. + */ + public final void closeAndClearTokenInformation() { + if (this.tokenCachingStrategy != null) { + this.tokenCachingStrategy.clear(); + } + Utility.clearFacebookCookies(staticContext); + Utility.clearCaches(staticContext); + close(); + } + + /** + * Adds a callback that will be called when the state of this Session changes. + * + * @param callback the callback + */ + public final void addCallback(StatusCallback callback) { + synchronized (callbacks) { + if (callback != null && !callbacks.contains(callback)) { + callbacks.add(callback); + } + } + } + + /** + * Removes a StatusCallback from this Session. + * + * @param callback the callback + */ + public final void removeCallback(StatusCallback callback) { + synchronized (callbacks) { + callbacks.remove(callback); + } + } + + @Override + public String toString() { + return new StringBuilder().append("{Session").append(" state:").append(this.state).append(", token:") + .append((this.tokenInfo == null) ? "null" : this.tokenInfo).append(", appId:") + .append((this.applicationId == null) ? "null" : this.applicationId).append("}").toString(); + } + + void extendTokenCompleted(Bundle bundle) { + synchronized (this.lock) { + final SessionState oldState = this.state; + + switch (this.state) { + case OPENED: + this.state = SessionState.OPENED_TOKEN_UPDATED; + postStateChange(oldState, this.state, null); + break; + case OPENED_TOKEN_UPDATED: + break; + default: + // Silently ignore attempts to refresh token if we are not open + Log.d(TAG, "refreshToken ignored in state " + this.state); + return; + } + this.tokenInfo = AccessToken.createFromRefresh(this.tokenInfo, bundle); + if (this.tokenCachingStrategy != null) { + this.tokenCachingStrategy.save(this.tokenInfo.toCacheBundle()); + } + } + } + + private Object writeReplace() { + return new SerializationProxyV1(applicationId, state, tokenInfo, + lastAttemptedTokenExtendDate, false, pendingAuthorizationRequest); + } + + // have a readObject that throws to prevent spoofing + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Cannot readObject, serialization proxy required"); + } + + /** + * Save the Session object into the supplied Bundle. This method is intended to be called from an + * Activity or Fragment's onSaveInstanceState method in order to preserve Sessions across Activity lifecycle events. + * + * @param session the Session to save + * @param bundle the Bundle to save the Session to + */ + public static final void saveSession(Session session, Bundle bundle) { + if (bundle != null && session != null && !bundle.containsKey(SESSION_BUNDLE_SAVE_KEY)) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + new ObjectOutputStream(outputStream).writeObject(session); + } catch (IOException e) { + throw new FacebookException("Unable to save session.", e); + } + bundle.putByteArray(SESSION_BUNDLE_SAVE_KEY, outputStream.toByteArray()); + bundle.putBundle(AUTH_BUNDLE_SAVE_KEY, session.authorizationBundle); + } + } + + /** + * Restores the saved session from a Bundle, if any. Returns the restored Session or + * null if it could not be restored. This method is intended to be called from an Activity or Fragment's + * onCreate method when a Session has previously been saved into a Bundle via saveState to preserve a Session + * across Activity lifecycle events. + * + * @param context the Activity or Service creating the Session, must not be null + * @param cachingStrategy the TokenCachingStrategy to use to load and store the token. If this is + * null, a default token cachingStrategy that stores data in + * SharedPreferences will be used + * @param callback the callback to notify for Session state changes, can be null + * @param bundle the bundle to restore the Session from + * @return the restored Session, or null + */ + public static final Session restoreSession( + Context context, TokenCachingStrategy cachingStrategy, StatusCallback callback, Bundle bundle) { + if (bundle == null) { + return null; + } + byte[] data = bundle.getByteArray(SESSION_BUNDLE_SAVE_KEY); + if (data != null) { + ByteArrayInputStream is = new ByteArrayInputStream(data); + try { + Session session = (Session) (new ObjectInputStream(is)).readObject(); + initializeStaticContext(context); + if (cachingStrategy != null) { + session.tokenCachingStrategy = cachingStrategy; + } else { + session.tokenCachingStrategy = new SharedPreferencesTokenCachingStrategy(context); + } + if (callback != null) { + session.addCallback(callback); + } + session.authorizationBundle = bundle.getBundle(AUTH_BUNDLE_SAVE_KEY); + return session; + } catch (ClassNotFoundException e) { + Log.w(TAG, "Unable to restore session", e); + } catch (IOException e) { + Log.w(TAG, "Unable to restore session.", e); + } + } + return null; + } + + + /** + * Returns the current active Session, or null if there is none. + * + * @return the current active Session, or null if there is none. + */ + public static final Session getActiveSession() { + synchronized (Session.STATIC_LOCK) { + return Session.activeSession; + } + } + + /** + *

+ * Sets the current active Session. + *

+ *

+ * The active Session is used implicitly by predefined Request factory + * methods as well as optionally by UI controls in the sdk. + *

+ *

+ * It is legal to set this to null, or to a Session that is not yet open. + *

+ * + * @param session A Session to use as the active Session, or null to indicate + * that there is no active Session. + */ + public static final void setActiveSession(Session session) { + synchronized (Session.STATIC_LOCK) { + if (session != Session.activeSession) { + Session oldSession = Session.activeSession; + + if (oldSession != null) { + oldSession.close(); + } + + Session.activeSession = session; + + if (oldSession != null) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_UNSET); + } + + if (session != null) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_SET); + + if (session.isOpened()) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED); + } + } + } + } + } + + /** + * If a cached token is available, creates and opens the session and makes it active without any user interaction, + * otherwise this does nothing. + * + * @param context The Context creating this session + * @return The new session or null if one could not be created + */ + public static Session openActiveSessionFromCache(Context context) { + return openActiveSession(context, false, null); + } + + /** + * If allowLoginUI is true, this will create a new Session, make it active, and + * open it. If the default token cache is not available, then this will request + * basic permissions. If the default token cache is available and cached tokens + * are loaded, this will use the cached token and associated permissions. + *

+ * If allowedLoginUI is false, this will only create the active session and open + * it if it requires no user interaction (i.e. the token cache is available and + * there are cached tokens). + * + * @param activity The Activity that is opening the new Session. + * @param allowLoginUI if false, only sets the active session and opens it if it + * does not require user interaction + * @param callback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. May be null. + * @return The new Session or null if one could not be created + */ + public static Session openActiveSession(Activity activity, boolean allowLoginUI, + StatusCallback callback) { + return openActiveSession(activity, allowLoginUI, new OpenRequest(activity).setCallback(callback)); + } + + /** + * If allowLoginUI is true, this will create a new Session, make it active, and + * open it. If the default token cache is not available, then this will request + * the permissions provided (and basic permissions of no permissions are provided). + * If the default token cache is available and cached tokens are loaded, this will + * use the cached token and associated permissions. + *

+ * If allowedLoginUI is false, this will only create the active session and open + * it if it requires no user interaction (i.e. the token cache is available and + * there are cached tokens). + * + * @param activity The Activity that is opening the new Session. + * @param allowLoginUI if false, only sets the active session and opens it if it + * does not require user interaction + * @param permissions The permissions to request for this Session + * @param callback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. May be null. + * @return The new Session or null if one could not be created + */ + public static Session openActiveSession(Activity activity, boolean allowLoginUI, + List permissions, StatusCallback callback) { + return openActiveSession( + activity, + allowLoginUI, + new OpenRequest(activity).setCallback(callback).setPermissions(permissions)); + } + + /** + * If allowLoginUI is true, this will create a new Session, make it active, and + * open it. If the default token cache is not available, then this will request + * basic permissions. If the default token cache is available and cached tokens + * are loaded, this will use the cached token and associated permissions. + *

+ * If allowedLoginUI is false, this will only create the active session and open + * it if it requires no user interaction (i.e. the token cache is available and + * there are cached tokens). + * + * @param context The Activity or Service creating this Session + * @param fragment The Fragment that is opening the new Session. + * @param allowLoginUI if false, only sets the active session and opens it if it + * does not require user interaction + * @param callback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. + * @return The new Session or null if one could not be created + */ + public static Session openActiveSession(Context context, Fragment fragment, + boolean allowLoginUI, StatusCallback callback) { + return openActiveSession(context, allowLoginUI, new OpenRequest(fragment).setCallback(callback)); + } + + /** + * If allowLoginUI is true, this will create a new Session, make it active, and + * open it. If the default token cache is not available, then this will request + * the permissions provided (and basic permissions of no permissions are provided). + * If the default token cache is available and cached tokens are loaded, this will + * use the cached token and associated permissions. + *

+ * If allowedLoginUI is false, this will only create the active session and open + * it if it requires no user interaction (i.e. the token cache is available and + * there are cached tokens). + * + * @param context The Activity or Service creating this Session + * @param fragment The Fragment that is opening the new Session. + * @param allowLoginUI if false, only sets the active session and opens it if it + * does not require user interaction + * @param permissions The permissions to request for this Session + * @param callback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. + * @return The new Session or null if one could not be created + */ + public static Session openActiveSession(Context context, Fragment fragment, + boolean allowLoginUI, List permissions, StatusCallback callback) { + return openActiveSession( + context, + allowLoginUI, + new OpenRequest(fragment).setCallback(callback).setPermissions(permissions)); + } + + /** + * Opens a session based on an existing Facebook access token, and also makes this session + * the currently active session. This method should be used + * only in instances where an application has previously obtained an access token and wishes + * to import it into the Session/TokenCachingStrategy-based session-management system. A primary + * example would be an application which previously did not use the Facebook SDK for Android + * and implemented its own session-management scheme, but wishes to implement an upgrade path + * for existing users so they do not need to log in again when upgrading to a version of + * the app that uses the SDK. In general, this method will be called only once, when the app + * detects that it has been upgraded -- after that, the usual Session lifecycle methods + * should be used to manage the session and its associated token. + *

+ * No validation is done that the token, token source, or permissions are actually valid. + * It is the caller's responsibility to ensure that these accurately reflect the state of + * the token that has been passed in, or calls to the Facebook API may fail. + * + * @param context the Context to use for creation the session + * @param accessToken the access token obtained from Facebook + * @param callback a callback that will be called when the session status changes; may be null + * @return The new Session or null if one could not be created + */ + public static Session openActiveSessionWithAccessToken(Context context, AccessToken accessToken, + StatusCallback callback) { + Session session = new Session(context, null, null, false); + + setActiveSession(session); + session.open(accessToken, callback); + + return session; + } + + private static Session openActiveSession(Context context, boolean allowLoginUI, OpenRequest openRequest) { + Session session = new Builder(context).build(); + if (SessionState.CREATED_TOKEN_LOADED.equals(session.getState()) || allowLoginUI) { + setActiveSession(session); + session.openForRead(openRequest); + return session; + } + return null; + } + + static Context getStaticContext() { + return staticContext; + } + + static void initializeStaticContext(Context currentContext) { + if ((currentContext != null) && (staticContext == null)) { + Context applicationContext = currentContext.getApplicationContext(); + staticContext = (applicationContext != null) ? applicationContext : currentContext; + } + } + + void authorize(AuthorizationRequest request) { + boolean started = false; + + request.setApplicationId(applicationId); + + autoPublishAsync(); + + logAuthorizationStart(); + + started = tryLoginActivity(request); + + pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LOGIN_ACTIVITY, + started ? AppEventsConstants.EVENT_PARAM_VALUE_YES : AppEventsConstants.EVENT_PARAM_VALUE_NO); + + if (!started && request.isLegacy) { + pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LEGACY, + AppEventsConstants.EVENT_PARAM_VALUE_YES); + + tryLegacyAuth(request); + started = true; + } + + if (!started) { + synchronized (this.lock) { + final SessionState oldState = this.state; + + switch (this.state) { + case CLOSED: + case CLOSED_LOGIN_FAILED: + return; + + default: + this.state = SessionState.CLOSED_LOGIN_FAILED; + + Exception exception = new FacebookException( + "Log in attempt failed: LoginActivity could not be started, and not legacy request"); + logAuthorizationComplete(AuthorizationClient.Result.Code.ERROR, null, exception); + postStateChange(oldState, this.state, exception); + } + } + } + } + + private void open(OpenRequest openRequest, SessionAuthorizationType authType) { + validatePermissions(openRequest, authType); + validateLoginBehavior(openRequest); + + SessionState newState; + synchronized (this.lock) { + if (pendingAuthorizationRequest != null) { + postStateChange(state, state, new UnsupportedOperationException( + "Session: an attempt was made to open a session that has a pending request.")); + return; + } + final SessionState oldState = this.state; + + switch (this.state) { + case CREATED: + this.state = newState = SessionState.OPENING; + if (openRequest == null) { + throw new IllegalArgumentException("openRequest cannot be null when opening a new Session"); + } + pendingAuthorizationRequest = openRequest; + break; + case CREATED_TOKEN_LOADED: + if (openRequest != null && !Utility.isNullOrEmpty(openRequest.getPermissions())) { + if (!Utility.isSubset(openRequest.getPermissions(), getPermissions())) { + pendingAuthorizationRequest = openRequest; + } + } + if (pendingAuthorizationRequest == null) { + this.state = newState = SessionState.OPENED; + } else { + this.state = newState = SessionState.OPENING; + } + break; + default: + throw new UnsupportedOperationException( + "Session: an attempt was made to open an already opened session."); + } + if (openRequest != null) { + addCallback(openRequest.getCallback()); + } + this.postStateChange(oldState, newState, null); + } + + if (newState == SessionState.OPENING) { + authorize(openRequest); + } + } + + private void requestNewPermissions(NewPermissionsRequest newPermissionsRequest, SessionAuthorizationType authType) { + validatePermissions(newPermissionsRequest, authType); + validateLoginBehavior(newPermissionsRequest); + + if (newPermissionsRequest != null) { + synchronized (this.lock) { + if (pendingAuthorizationRequest != null) { + throw new UnsupportedOperationException( + "Session: an attempt was made to request new permissions for a session that has a pending request."); + } + if (state.isOpened()) { + pendingAuthorizationRequest = newPermissionsRequest; + } else if (state.isClosed()) { + throw new UnsupportedOperationException( + "Session: an attempt was made to request new permissions for a session that has been closed."); + } else { + throw new UnsupportedOperationException( + "Session: an attempt was made to request new permissions for a session that is not currently open."); + } + } + + newPermissionsRequest.setValidateSameFbidAsToken(getAccessToken()); + addCallback(newPermissionsRequest.getCallback()); + authorize(newPermissionsRequest); + } + } + + private void validateLoginBehavior(AuthorizationRequest request) { + if (request != null && !request.isLegacy) { + Intent intent = new Intent(); + intent.setClass(getStaticContext(), LoginActivity.class); + if (!resolveIntent(intent)) { + throw new FacebookException(String.format( + "Cannot use SessionLoginBehavior %s when %s is not declared as an activity in AndroidManifest.xml", + request.getLoginBehavior(), LoginActivity.class.getName())); + } + } + } + + private void validatePermissions(AuthorizationRequest request, SessionAuthorizationType authType) { + if (request == null || Utility.isNullOrEmpty(request.getPermissions())) { + if (SessionAuthorizationType.PUBLISH.equals(authType)) { + throw new FacebookException("Cannot request publish or manage authorization with no permissions."); + } + return; // nothing to check + } + for (String permission : request.getPermissions()) { + if (isPublishPermission(permission)) { + if (SessionAuthorizationType.READ.equals(authType)) { + throw new FacebookException( + String.format( + "Cannot pass a publish or manage permission (%s) to a request for read authorization", + permission)); + } + } else { + if (SessionAuthorizationType.PUBLISH.equals(authType)) { + Log.w(TAG, + String.format( + "Should not pass a read permission (%s) to a request for publish or manage authorization", + permission)); + } + } + } + } + + public static boolean isPublishPermission(String permission) { + return permission != null && + (permission.startsWith(PUBLISH_PERMISSION_PREFIX) || + permission.startsWith(MANAGE_PERMISSION_PREFIX) || + OTHER_PUBLISH_PERMISSIONS.contains(permission)); + + } + + private void handleAuthorizationResult(int resultCode, AuthorizationClient.Result result) { + AccessToken newToken = null; + Exception exception = null; + if (resultCode == Activity.RESULT_OK) { + if (result.code == AuthorizationClient.Result.Code.SUCCESS) { + newToken = result.token; + } else { + exception = new FacebookAuthorizationException(result.errorMessage); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + exception = new FacebookOperationCanceledException(result.errorMessage); + } + + logAuthorizationComplete(result.code, result.loggingExtras, exception); + + authorizationClient = null; + finishAuthOrReauth(newToken, exception); + } + + private void logAuthorizationStart() { + Bundle bundle = AuthorizationClient.newAuthorizationLoggingBundle(pendingAuthorizationRequest.getAuthId()); + bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); + + // Log what we already know about the call in start event + try { + JSONObject extras = new JSONObject(); + extras.put(AuthorizationClient.EVENT_EXTRAS_LOGIN_BEHAVIOR, + pendingAuthorizationRequest.loginBehavior.toString()); + extras.put(AuthorizationClient.EVENT_EXTRAS_REQUEST_CODE, pendingAuthorizationRequest.requestCode); + extras.put(AuthorizationClient.EVENT_EXTRAS_IS_LEGACY, pendingAuthorizationRequest.isLegacy); + extras.put(AuthorizationClient.EVENT_EXTRAS_PERMISSIONS, + TextUtils.join(",", pendingAuthorizationRequest.permissions)); + extras.put(AuthorizationClient.EVENT_EXTRAS_DEFAULT_AUDIENCE, + pendingAuthorizationRequest.defaultAudience.toString()); + bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, extras.toString()); + } catch (JSONException e) { + } + + AppEventsLogger logger = getAppEventsLogger(); + logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_START, null, bundle); + } + + private void logAuthorizationComplete(AuthorizationClient.Result.Code result, Map resultExtras, + Exception exception) { + Bundle bundle = null; + if (pendingAuthorizationRequest == null) { + // We don't expect this to happen, but if it does, log an event for diagnostic purposes. + bundle = AuthorizationClient.newAuthorizationLoggingBundle(""); + bundle.putString(AuthorizationClient.EVENT_PARAM_LOGIN_RESULT, + AuthorizationClient.Result.Code.ERROR.getLoggingValue()); + bundle.putString(AuthorizationClient.EVENT_PARAM_ERROR_MESSAGE, + "Unexpected call to logAuthorizationComplete with null pendingAuthorizationRequest."); + } else { + bundle = AuthorizationClient.newAuthorizationLoggingBundle(pendingAuthorizationRequest.getAuthId()); + if (result != null) { + bundle.putString(AuthorizationClient.EVENT_PARAM_LOGIN_RESULT, result.getLoggingValue()); + } + if (exception != null && exception.getMessage() != null) { + bundle.putString(AuthorizationClient.EVENT_PARAM_ERROR_MESSAGE, exception.getMessage()); + } + + // Combine extras from the request and from the result. + JSONObject jsonObject = null; + if (pendingAuthorizationRequest.loggingExtras.isEmpty() == false) { + jsonObject = new JSONObject(pendingAuthorizationRequest.loggingExtras); + } + if (resultExtras != null) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + try { + for (Map.Entry entry : resultExtras.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + } catch (JSONException e) { + } + } + if (jsonObject != null) { + bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, jsonObject.toString()); + } + } + bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); + + AppEventsLogger logger = getAppEventsLogger(); + logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_COMPLETE, null, bundle); + } + + private boolean tryLoginActivity(AuthorizationRequest request) { + Intent intent = getLoginActivityIntent(request); + + if (!resolveIntent(intent)) { + return false; + } + + try { + request.getStartActivityDelegate().startActivityForResult(intent, request.getRequestCode()); + } catch (ActivityNotFoundException e) { + return false; + } + + return true; + } + + private boolean resolveIntent(Intent intent) { + ResolveInfo resolveInfo = getStaticContext().getPackageManager().resolveActivity(intent, 0); + if (resolveInfo == null) { + return false; + } + return true; + } + + private Intent getLoginActivityIntent(AuthorizationRequest request) { + Intent intent = new Intent(); + intent.setClass(getStaticContext(), LoginActivity.class); + intent.setAction(request.getLoginBehavior().toString()); + + // Let LoginActivity populate extras appropriately + AuthorizationClient.AuthorizationRequest authClientRequest = request.getAuthorizationClientRequest(); + Bundle extras = LoginActivity.populateIntentExtras(authClientRequest); + intent.putExtras(extras); + + return intent; + } + + private void tryLegacyAuth(final AuthorizationRequest request) { + authorizationClient = new AuthorizationClient(); + authorizationClient.setOnCompletedListener(new AuthorizationClient.OnCompletedListener() { + @Override + public void onCompleted(AuthorizationClient.Result result) { + int activityResult; + if (result.code == AuthorizationClient.Result.Code.CANCEL) { + activityResult = Activity.RESULT_CANCELED; + } else { + activityResult = Activity.RESULT_OK; + } + handleAuthorizationResult(activityResult, result); + } + }); + authorizationClient.setContext(getStaticContext()); + authorizationClient.startOrContinueAuth(request.getAuthorizationClientRequest()); + } + + void finishAuthOrReauth(AccessToken newToken, Exception exception) { + // If the token we came up with is expired/invalid, then auth failed. + if ((newToken != null) && newToken.isInvalid()) { + newToken = null; + exception = new FacebookException("Invalid access token."); + } + + + synchronized (this.lock) { + switch (this.state) { + case OPENING: + // This means we are authorizing for the first time in this Session. + finishAuthorization(newToken, exception); + break; + + case OPENED: + case OPENED_TOKEN_UPDATED: + // This means we are reauthorizing. + finishReauthorization(newToken, exception); + break; + + case CREATED: + case CREATED_TOKEN_LOADED: + case CLOSED: + case CLOSED_LOGIN_FAILED: + Log.d(TAG, "Unexpected call to finishAuthOrReauth in state " + this.state); + break; + } + } + } + + private void finishAuthorization(AccessToken newToken, Exception exception) { + final SessionState oldState = state; + if (newToken != null) { + tokenInfo = newToken; + saveTokenToCache(newToken); + + state = SessionState.OPENED; + } else if (exception != null) { + state = SessionState.CLOSED_LOGIN_FAILED; + } + pendingAuthorizationRequest = null; + postStateChange(oldState, state, exception); + } + + private void finishReauthorization(final AccessToken newToken, Exception exception) { + final SessionState oldState = state; + + if (newToken != null) { + tokenInfo = newToken; + saveTokenToCache(newToken); + + state = SessionState.OPENED_TOKEN_UPDATED; + } + + pendingAuthorizationRequest = null; + postStateChange(oldState, state, exception); + } + + private void saveTokenToCache(AccessToken newToken) { + if (newToken != null && tokenCachingStrategy != null) { + tokenCachingStrategy.save(newToken.toCacheBundle()); + } + } + + void postStateChange(final SessionState oldState, final SessionState newState, final Exception exception) { + // When we request new permissions, we stay in SessionState.OPENED_TOKEN_UPDATED, + // but we still want notifications of the state change since permissions are + // different now. + if ((oldState == newState) && + (oldState != SessionState.OPENED_TOKEN_UPDATED) && + (exception == null)) { + return; + } + + if (newState.isClosed()) { + this.tokenInfo = AccessToken.createEmptyToken(); + } + + // Need to schedule the callbacks inside the same queue to preserve ordering. + // Otherwise these callbacks could have been added to the queue before the SessionTracker + // gets the ACTIVE_SESSION_SET action. + Runnable runCallbacks = new Runnable() { + public void run() { + synchronized (callbacks) { + for (final StatusCallback callback : callbacks) { + Runnable closure = new Runnable() { + public void run() { + // This can be called inside a synchronized block. + callback.call(Session.this, newState, exception); + } + }; + + runWithHandlerOrExecutor(handler, closure); + } + } + } + }; + runWithHandlerOrExecutor(handler, runCallbacks); + + if (this == Session.activeSession) { + if (oldState.isOpened() != newState.isOpened()) { + if (newState.isOpened()) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED); + } else { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_CLOSED); + } + } + } + } + + static void postActiveSessionAction(String action) { + final Intent intent = new Intent(action); + + LocalBroadcastManager.getInstance(getStaticContext()).sendBroadcast(intent); + } + + private static void runWithHandlerOrExecutor(Handler handler, Runnable runnable) { + if (handler != null) { + handler.post(runnable); + } else { + Settings.getExecutor().execute(runnable); + } + } + + void extendAccessTokenIfNeeded() { + if (shouldExtendAccessToken()) { + extendAccessToken(); + } + } + + void extendAccessToken() { + TokenRefreshRequest newTokenRefreshRequest = null; + synchronized (this.lock) { + if (currentTokenRefreshRequest == null) { + newTokenRefreshRequest = new TokenRefreshRequest(); + currentTokenRefreshRequest = newTokenRefreshRequest; + } + } + + if (newTokenRefreshRequest != null) { + newTokenRefreshRequest.bind(); + } + } + + boolean shouldExtendAccessToken() { + if (currentTokenRefreshRequest != null) { + return false; + } + + boolean result = false; + + Date now = new Date(); + + if (state.isOpened() && tokenInfo.getSource().canExtendToken() + && now.getTime() - lastAttemptedTokenExtendDate.getTime() > TOKEN_EXTEND_RETRY_SECONDS * 1000 + && now.getTime() - tokenInfo.getLastRefresh().getTime() > TOKEN_EXTEND_THRESHOLD_SECONDS * 1000) { + result = true; + } + + return result; + } + + private AppEventsLogger getAppEventsLogger() { + synchronized (lock) { + if (appEventsLogger == null) { + appEventsLogger = AppEventsLogger.newLogger(staticContext, applicationId); + } + return appEventsLogger; + } + } + + AccessToken getTokenInfo() { + return tokenInfo; + } + + void setTokenInfo(AccessToken tokenInfo) { + this.tokenInfo = tokenInfo; + } + + Date getLastAttemptedTokenExtendDate() { + return lastAttemptedTokenExtendDate; + } + + void setLastAttemptedTokenExtendDate(Date lastAttemptedTokenExtendDate) { + this.lastAttemptedTokenExtendDate = lastAttemptedTokenExtendDate; + } + + void setCurrentTokenRefreshRequest(TokenRefreshRequest request) { + this.currentTokenRefreshRequest = request; + } + + class TokenRefreshRequest implements ServiceConnection { + + final Messenger messageReceiver = new Messenger( + new TokenRefreshRequestHandler(Session.this, this)); + + Messenger messageSender = null; + + public void bind() { + Intent intent = NativeProtocol.createTokenRefreshIntent(getStaticContext()); + if (intent != null + && staticContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) { + setLastAttemptedTokenExtendDate(new Date()); + } else { + cleanup(); + } + } + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + messageSender = new Messenger(service); + refreshToken(); + } + + @Override + public void onServiceDisconnected(ComponentName arg) { + cleanup(); + + // We returned an error so there's no point in + // keeping the binding open. + staticContext.unbindService(TokenRefreshRequest.this); + } + + private void cleanup() { + if (currentTokenRefreshRequest == this) { + currentTokenRefreshRequest = null; + } + } + + private void refreshToken() { + Bundle requestData = new Bundle(); + requestData.putString(AccessToken.ACCESS_TOKEN_KEY, getTokenInfo().getToken()); + + Message request = Message.obtain(); + request.setData(requestData); + request.replyTo = messageReceiver; + + try { + messageSender.send(request); + } catch (RemoteException e) { + cleanup(); + } + } + + } + + // Creating a static Handler class to reduce the possibility of a memory leak. + // Handler objects for the same thread all share a common Looper object, which they post messages + // to and read from. As messages contain target Handler, as long as there are messages with target + // handler in the message queue, the handler cannot be garbage collected. If handler is not static, + // the instance of the containing class also cannot be garbage collected even if it is destroyed. + static class TokenRefreshRequestHandler extends Handler { + + private WeakReference sessionWeakReference; + private WeakReference refreshRequestWeakReference; + + TokenRefreshRequestHandler(Session session, TokenRefreshRequest refreshRequest) { + super(Looper.getMainLooper()); + sessionWeakReference = new WeakReference(session); + refreshRequestWeakReference = new WeakReference(refreshRequest); + } + + @Override + public void handleMessage(Message msg) { + String token = msg.getData().getString(AccessToken.ACCESS_TOKEN_KEY); + Session session = sessionWeakReference.get(); + + if (session != null && token != null) { + session.extendTokenCompleted(msg.getData()); + } + + TokenRefreshRequest request = refreshRequestWeakReference.get(); + if (request != null) { + // The refreshToken function should be called rarely, + // so there is no point in keeping the binding open. + staticContext.unbindService(request); + request.cleanup(); + } + } + } + + /** + * Provides asynchronous notification of Session state changes. + * + * @see Session#open open + */ + public interface StatusCallback { + public void call(Session session, SessionState state, Exception exception); + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean equals(Object otherObj) { + if (!(otherObj instanceof Session)) { + return false; + } + Session other = (Session) otherObj; + + return areEqual(other.applicationId, applicationId) && + areEqual(other.authorizationBundle, authorizationBundle) && + areEqual(other.state, state) && + areEqual(other.getExpirationDate(), getExpirationDate()); + } + + private static boolean areEqual(Object a, Object b) { + if (a == null) { + return b == null; + } else { + return a.equals(b); + } + } + + /** + * Builder class used to create a Session. + */ + public static final class Builder { + private final Context context; + private String applicationId; + private TokenCachingStrategy tokenCachingStrategy; + + /** + * Constructs a new Builder associated with the context. + * + * @param context the Activity or Service starting the Session + */ + public Builder(Context context) { + this.context = context; + } + + /** + * Sets the application id for the Session. + * + * @param applicationId the application id + * @return the Builder instance + */ + public Builder setApplicationId(final String applicationId) { + this.applicationId = applicationId; + return this; + } + + /** + * Sets the TokenCachingStrategy for the Session. + * + * @param tokenCachingStrategy the token cache to use + * @return the Builder instance + */ + public Builder setTokenCachingStrategy(final TokenCachingStrategy tokenCachingStrategy) { + this.tokenCachingStrategy = tokenCachingStrategy; + return this; + } + + /** + * Build the Session. + * + * @return a new Session + */ + public Session build() { + return new Session(context, applicationId, tokenCachingStrategy); + } + } + + interface StartActivityDelegate { + public void startActivityForResult(Intent intent, int requestCode); + + public Activity getActivityContext(); + } + + @SuppressWarnings("deprecation") + private void autoPublishAsync() { + AutoPublishAsyncTask asyncTask = null; + synchronized (this) { + if (autoPublishAsyncTask == null && Settings.getShouldAutoPublishInstall()) { + // copy the application id to guarantee thread safety against our container. + String applicationId = Session.this.applicationId; + + // skip publish if we don't have an application id. + if (applicationId != null) { + asyncTask = autoPublishAsyncTask = new AutoPublishAsyncTask(applicationId, staticContext); + } + } + } + + if (asyncTask != null) { + asyncTask.execute(); + } + } + + /** + * Async implementation to allow auto publishing to not block the ui thread. + */ + private class AutoPublishAsyncTask extends AsyncTask { + private final String mApplicationId; + private final Context mApplicationContext; + + public AutoPublishAsyncTask(String applicationId, Context context) { + mApplicationId = applicationId; + mApplicationContext = context.getApplicationContext(); + } + + @Override + protected Void doInBackground(Void... voids) { + try { + Settings.publishInstallAndWaitForResponse(mApplicationContext, mApplicationId, true); + } catch (Exception e) { + Utility.logd("Facebook-publish", e); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + // always clear out the publisher to allow other invocations. + synchronized (Session.this) { + autoPublishAsyncTask = null; + } + } + } + + /** + * Base class for authorization requests {@link OpenRequest} and {@link NewPermissionsRequest}. + */ + public static class AuthorizationRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + private final StartActivityDelegate startActivityDelegate; + private SessionLoginBehavior loginBehavior = SessionLoginBehavior.SSO_WITH_FALLBACK; + private int requestCode = DEFAULT_AUTHORIZE_ACTIVITY_CODE; + private StatusCallback statusCallback; + private boolean isLegacy = false; + private List permissions = Collections.emptyList(); + private SessionDefaultAudience defaultAudience = SessionDefaultAudience.FRIENDS; + private String applicationId; + private String validateSameFbidAsToken; + private final String authId = UUID.randomUUID().toString(); + private final Map loggingExtras = new HashMap(); + + AuthorizationRequest(final Activity activity) { + startActivityDelegate = new StartActivityDelegate() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { + activity.startActivityForResult(intent, requestCode); + } + + @Override + public Activity getActivityContext() { + return activity; + } + }; + } + + AuthorizationRequest(final Fragment fragment) { + startActivityDelegate = new StartActivityDelegate() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { + fragment.startActivityForResult(intent, requestCode); + } + + @Override + public Activity getActivityContext() { + return fragment.getActivity(); + } + }; + } + + /** + * Constructor to be used for V1 serialization only, DO NOT CHANGE. + */ + private AuthorizationRequest(SessionLoginBehavior loginBehavior, int requestCode, + List permissions, String defaultAudience, boolean isLegacy, String applicationId, + String validateSameFbidAsToken) { + startActivityDelegate = new StartActivityDelegate() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { + throw new UnsupportedOperationException( + "Cannot create an AuthorizationRequest without a valid Activity or Fragment"); + } + + @Override + public Activity getActivityContext() { + throw new UnsupportedOperationException( + "Cannot create an AuthorizationRequest without a valid Activity or Fragment"); + } + }; + this.loginBehavior = loginBehavior; + this.requestCode = requestCode; + this.permissions = permissions; + this.defaultAudience = SessionDefaultAudience.valueOf(defaultAudience); + this.isLegacy = isLegacy; + this.applicationId = applicationId; + this.validateSameFbidAsToken = validateSameFbidAsToken; + } + + /** + * Used for backwards compatibility with Facebook.java only, DO NOT USE. + * + * @param isLegacy + */ + public void setIsLegacy(boolean isLegacy) { + this.isLegacy = isLegacy; + } + + boolean isLegacy() { + return isLegacy; + } + + AuthorizationRequest setCallback(StatusCallback statusCallback) { + this.statusCallback = statusCallback; + return this; + } + + StatusCallback getCallback() { + return statusCallback; + } + + AuthorizationRequest setLoginBehavior(SessionLoginBehavior loginBehavior) { + if (loginBehavior != null) { + this.loginBehavior = loginBehavior; + } + return this; + } + + SessionLoginBehavior getLoginBehavior() { + return loginBehavior; + } + + AuthorizationRequest setRequestCode(int requestCode) { + if (requestCode >= 0) { + this.requestCode = requestCode; + } + return this; + } + + int getRequestCode() { + return requestCode; + } + + AuthorizationRequest setPermissions(List permissions) { + if (permissions != null) { + this.permissions = permissions; + } + return this; + } + + AuthorizationRequest setPermissions(String... permissions) { + return setPermissions(Arrays.asList(permissions)); + } + + List getPermissions() { + return permissions; + } + + AuthorizationRequest setDefaultAudience(SessionDefaultAudience defaultAudience) { + if (defaultAudience != null) { + this.defaultAudience = defaultAudience; + } + return this; + } + + SessionDefaultAudience getDefaultAudience() { + return defaultAudience; + } + + StartActivityDelegate getStartActivityDelegate() { + return startActivityDelegate; + } + + String getApplicationId() { + return applicationId; + } + + void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + String getValidateSameFbidAsToken() { + return validateSameFbidAsToken; + } + + void setValidateSameFbidAsToken(String validateSameFbidAsToken) { + this.validateSameFbidAsToken = validateSameFbidAsToken; + } + + String getAuthId() { + return authId; + } + + AuthorizationClient.AuthorizationRequest getAuthorizationClientRequest() { + AuthorizationClient.StartActivityDelegate delegate = new AuthorizationClient.StartActivityDelegate() { + @Override + public void startActivityForResult(Intent intent, int requestCode) { + startActivityDelegate.startActivityForResult(intent, requestCode); + } + + @Override + public Activity getActivityContext() { + return startActivityDelegate.getActivityContext(); + } + }; + return new AuthorizationClient.AuthorizationRequest(loginBehavior, requestCode, isLegacy, + permissions, defaultAudience, applicationId, validateSameFbidAsToken, delegate, authId); + } + + // package private so subclasses can use it + Object writeReplace() { + return new AuthRequestSerializationProxyV1( + loginBehavior, requestCode, permissions, defaultAudience.name(), isLegacy, applicationId, validateSameFbidAsToken); + } + + // have a readObject that throws to prevent spoofing; must be private so serializer will call it (will be + // called automatically prior to any base class) + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Cannot readObject, serialization proxy required"); + } + + private static class AuthRequestSerializationProxyV1 implements Serializable { + private static final long serialVersionUID = -8748347685113614927L; + private final SessionLoginBehavior loginBehavior; + private final int requestCode; + private boolean isLegacy; + private final List permissions; + private final String defaultAudience; + private final String applicationId; + private final String validateSameFbidAsToken; + + private AuthRequestSerializationProxyV1(SessionLoginBehavior loginBehavior, + int requestCode, List permissions, String defaultAudience, boolean isLegacy, + String applicationId, String validateSameFbidAsToken) { + this.loginBehavior = loginBehavior; + this.requestCode = requestCode; + this.permissions = permissions; + this.defaultAudience = defaultAudience; + this.isLegacy = isLegacy; + this.applicationId = applicationId; + this.validateSameFbidAsToken = validateSameFbidAsToken; + } + + private Object readResolve() { + return new AuthorizationRequest(loginBehavior, requestCode, permissions, defaultAudience, isLegacy, + applicationId, validateSameFbidAsToken); + } + } + } + + /** + * A request used to open a Session. + */ + public static final class OpenRequest extends AuthorizationRequest { + private static final long serialVersionUID = 1L; + + /** + * Constructs an OpenRequest. + * + * @param activity the Activity to use to open the Session + */ + public OpenRequest(Activity activity) { + super(activity); + } + + /** + * Constructs an OpenRequest. + * + * @param fragment the Fragment to use to open the Session + */ + public OpenRequest(Fragment fragment) { + super(fragment); + } + + /** + * Sets the StatusCallback for the OpenRequest. + * + * @param statusCallback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. + * @return the OpenRequest object to allow for chaining + */ + public final OpenRequest setCallback(StatusCallback statusCallback) { + super.setCallback(statusCallback); + return this; + } + + /** + * Sets the login behavior for the OpenRequest. + * + * @param loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + * @return the OpenRequest object to allow for chaining + */ + public final OpenRequest setLoginBehavior(SessionLoginBehavior loginBehavior) { + super.setLoginBehavior(loginBehavior); + return this; + } + + /** + * Sets the request code for the OpenRequest. + * + * @param requestCode An integer that identifies this request. This integer will be used + * as the request code in {@link Activity#onActivityResult + * onActivityResult}. This integer should be >= 0. If a value < 0 is + * passed in, then a default value will be used. + * @return the OpenRequest object to allow for chaining + */ + public final OpenRequest setRequestCode(int requestCode) { + super.setRequestCode(requestCode); + return this; + } + + /** + * Sets the permissions for the OpenRequest. + * + * @param permissions A List<String> representing the permissions to request + * during the authentication flow. A null or empty List + * represents basic permissions. + * @return the OpenRequest object to allow for chaining + */ + public final OpenRequest setPermissions(List permissions) { + super.setPermissions(permissions); + return this; + } + + /** + * Sets the permissions for the OpenRequest. + * + * @param permissions the permissions to request during the authentication flow. + * @return the OpenRequest object to allow for chaining + */ + public final OpenRequest setPermissions(String... permissions) { + super.setPermissions(permissions); + return this; + } + + /** + * Sets the defaultAudience for the OpenRequest. + *

+ * This is only used during Native login using a sufficiently recent facebook app. + * + * @param defaultAudience A SessionDefaultAudience representing the default audience setting to request. + * @return the OpenRequest object to allow for chaining + */ + public final OpenRequest setDefaultAudience(SessionDefaultAudience defaultAudience) { + super.setDefaultAudience(defaultAudience); + return this; + } + } + + /** + * A request to be used to request new permissions for a Session. + */ + public static final class NewPermissionsRequest extends AuthorizationRequest { + private static final long serialVersionUID = 1L; + + /** + * Constructs a NewPermissionsRequest. + * + * @param activity the Activity used to issue the request + * @param permissions additional permissions to request + */ + public NewPermissionsRequest(Activity activity, List permissions) { + super(activity); + setPermissions(permissions); + } + + /** + * Constructs a NewPermissionsRequest. + * + * @param fragment the Fragment used to issue the request + * @param permissions additional permissions to request + */ + public NewPermissionsRequest(Fragment fragment, List permissions) { + super(fragment); + setPermissions(permissions); + } + + /** + * Constructs a NewPermissionsRequest. + * + * @param activity the Activity used to issue the request + * @param permissions additional permissions to request + */ + public NewPermissionsRequest(Activity activity, String... permissions) { + super(activity); + setPermissions(permissions); + } + + /** + * Constructs a NewPermissionsRequest. + * + * @param fragment the Fragment used to issue the request + * @param permissions additional permissions to request + */ + public NewPermissionsRequest(Fragment fragment, String... permissions) { + super(fragment); + setPermissions(permissions); + } + + /** + * Sets the StatusCallback for the NewPermissionsRequest. Note that once the request is made, this callback + * will be added to the session, and will receive all future state changes on the session. + * + * @param statusCallback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. + * @return the NewPermissionsRequest object to allow for chaining + */ + public final NewPermissionsRequest setCallback(StatusCallback statusCallback) { + super.setCallback(statusCallback); + return this; + } + + /** + * Sets the login behavior for the NewPermissionsRequest. + * + * @param loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + * @return the NewPermissionsRequest object to allow for chaining + */ + public final NewPermissionsRequest setLoginBehavior(SessionLoginBehavior loginBehavior) { + super.setLoginBehavior(loginBehavior); + return this; + } + + /** + * Sets the request code for the NewPermissionsRequest. + * + * @param requestCode An integer that identifies this request. This integer will be used + * as the request code in {@link Activity#onActivityResult + * onActivityResult}. This integer should be >= 0. If a value < 0 is + * passed in, then a default value will be used. + * @return the NewPermissionsRequest object to allow for chaining + */ + public final NewPermissionsRequest setRequestCode(int requestCode) { + super.setRequestCode(requestCode); + return this; + } + + /** + * Sets the defaultAudience for the OpenRequest. + * + * @param defaultAudience A SessionDefaultAudience representing the default audience setting to request. + * @return the NewPermissionsRequest object to allow for chaining + */ + public final NewPermissionsRequest setDefaultAudience(SessionDefaultAudience defaultAudience) { + super.setDefaultAudience(defaultAudience); + return this; + } + + @Override + AuthorizationClient.AuthorizationRequest getAuthorizationClientRequest() { + AuthorizationClient.AuthorizationRequest request = super.getAuthorizationClientRequest(); + request.setRerequest(true); + return request; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionDefaultAudience.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionDefaultAudience.java new file mode 100644 index 0000000000..2fdac3d013 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionDefaultAudience.java @@ -0,0 +1,57 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import com.facebook.internal.NativeProtocol; + +/** + * Certain operations such as publishing a status or publishing a photo require an audience. When the user + * grants an application permission to perform a publish operation, a default audience is selected as the + * publication ceiling for the application. This enumerated value allows the application to select which + * audience to ask the user to grant publish permission for. + */ +public enum SessionDefaultAudience { + /** + * Represents an invalid default audience value, can be used when only reading. + */ + NONE(null), + + /** + * Indicates only the user is able to see posts made by the application. + */ + ONLY_ME(NativeProtocol.AUDIENCE_ME), + + /** + * Indicates that the user's friends are able to see posts made by the application. + */ + FRIENDS(NativeProtocol.AUDIENCE_FRIENDS), + + /** + * Indicates that all Facebook users are able to see posts made by the application. + */ + EVERYONE(NativeProtocol.AUDIENCE_EVERYONE); + + private final String nativeProtocolAudience; + + private SessionDefaultAudience(String protocol) { + nativeProtocolAudience = protocol; + } + + String getNativeProtocolAudience() { + return nativeProtocolAudience; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionLoginBehavior.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionLoginBehavior.java new file mode 100644 index 0000000000..17dd09658c --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionLoginBehavior.java @@ -0,0 +1,59 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + * Specifies the behaviors to try during + * {@link Session#openForRead(com.facebook.Session.OpenRequest) openForRead}, + * {@link Session#openForPublish(com.facebook.Session.OpenRequest) openForPublish}, + * {@link Session#requestNewReadPermissions(com.facebook.Session.NewPermissionsRequest) requestNewReadPermissions}, or + * {@link Session#requestNewPublishPermissions(com.facebook.Session.NewPermissionsRequest) requestNewPublishPermissions}. + */ +public enum SessionLoginBehavior { + /** + * Specifies that Session should attempt Single Sign On (SSO), and if that + * does not work fall back to dialog auth. This is the default behavior. + */ + SSO_WITH_FALLBACK(true, true), + + /** + * Specifies that Session should only attempt SSO. If SSO fails, then the + * open or new permissions call fails. + */ + SSO_ONLY(true, false), + + /** + * Specifies that SSO should not be attempted, and to only use dialog auth. + */ + SUPPRESS_SSO(false, true); + + private final boolean allowsKatanaAuth; + private final boolean allowsWebViewAuth; + + private SessionLoginBehavior(boolean allowsKatanaAuth, boolean allowsWebViewAuth) { + this.allowsKatanaAuth = allowsKatanaAuth; + this.allowsWebViewAuth = allowsWebViewAuth; + } + + boolean allowsKatanaAuth() { + return allowsKatanaAuth; + } + + boolean allowsWebViewAuth() { + return allowsWebViewAuth; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionState.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionState.java new file mode 100644 index 0000000000..99ac701877 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SessionState.java @@ -0,0 +1,117 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +/** + *

+ * Identifies the state of a Session. + *

+ *

+ * Session objects implement a state machine that controls their lifecycle. This + * enum represents the states of the state machine. + *

+ */ +public enum SessionState { + /** + * Indicates that the Session has not yet been opened and has no cached + * token. Opening a Session in this state will involve user interaction. + */ + CREATED(Category.CREATED_CATEGORY), + + /** + *

+ * Indicates that the Session has not yet been opened and has a cached + * token. Opening a Session in this state will not involve user interaction. + *

+ *

+ * If you are using Session from an Android Service, you must provide a + * TokenCachingStrategy implementation that contains a valid token to the Session + * constructor. The resulting Session will be created in this state, and you + * can then safely call open, passing null for the Activity. + *

+ */ + CREATED_TOKEN_LOADED(Category.CREATED_CATEGORY), + + /** + * Indicates that the Session is in the process of opening. + */ + OPENING(Category.CREATED_CATEGORY), + + /** + * Indicates that the Session is opened. In this state, the Session may be + * used with a {@link Request}. + */ + OPENED(Category.OPENED_CATEGORY), + + /** + *

+ * Indicates that the Session is opened and that the token has changed. In + * this state, the Session may be used with {@link Request}. + *

+ *

+ * Every time the token is updated, {@link Session.StatusCallback + * StatusCallback} is called with this value. + *

+ */ + OPENED_TOKEN_UPDATED(Category.OPENED_CATEGORY), + + /** + * Indicates that the Session is closed, and that it was not closed + * normally. Typically this means that the open call failed, and the + * Exception parameter to {@link Session.StatusCallback StatusCallback} will + * be non-null. + */ + CLOSED_LOGIN_FAILED(Category.CLOSED_CATEGORY), + + /** + * Indicates that the Session was closed normally. + */ + CLOSED(Category.CLOSED_CATEGORY); + + private final Category category; + + SessionState(Category category) { + this.category = category; + } + + /** + * Returns a boolean indicating whether the state represents a successfully + * opened state in which the Session can be used with a {@link Request}. + * + * @return a boolean indicating whether the state represents a successfully + * opened state in which the Session can be used with a + * {@link Request}. + */ + public boolean isOpened() { + return this.category == Category.OPENED_CATEGORY; + } + + /** + * Returns a boolean indicating whether the state represents a closed + * Session that can no longer be used with a {@link Request}. + * + * @return a boolean indicating whether the state represents a closed + * Session that can no longer be used with a {@link Request}. + */ + public boolean isClosed() { + return this.category == Category.CLOSED_CATEGORY; + } + + private enum Category { + CREATED_CATEGORY, OPENED_CATEGORY, CLOSED_CATEGORY + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/Settings.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Settings.java new file mode 100644 index 0000000000..cc1eb033bb --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/Settings.java @@ -0,0 +1,620 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import com.facebook.android.BuildConfig; +import com.facebook.internal.AttributionIdentifiers; +import com.facebook.internal.Utility; +import com.facebook.model.GraphObject; +import com.facebook.internal.Validate; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Allows some customization of sdk behavior. + */ +public final class Settings { + private static final String TAG = Settings.class.getCanonicalName(); + private static final HashSet loggingBehaviors = + new HashSet(Arrays.asList(LoggingBehavior.DEVELOPER_ERRORS)); + private static volatile Executor executor; + private static volatile boolean shouldAutoPublishInstall; + private static volatile String appVersion; + private static volatile String applicationId; + private static volatile String appClientToken; + private static volatile boolean defaultsLoaded = false; + private static final String FACEBOOK_COM = "facebook.com"; + private static volatile String facebookDomain = FACEBOOK_COM; + private static AtomicLong onProgressThreshold = new AtomicLong(65536); + private static volatile boolean platformCompatibilityEnabled; + + private static final int DEFAULT_CORE_POOL_SIZE = 5; + private static final int DEFAULT_MAXIMUM_POOL_SIZE = 128; + private static final int DEFAULT_KEEP_ALIVE = 1; + private static final Object LOCK = new Object(); + + private static final Uri ATTRIBUTION_ID_CONTENT_URI = + Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider"); + private static final String ATTRIBUTION_ID_COLUMN_NAME = "aid"; + + private static final String ATTRIBUTION_PREFERENCES = "com.facebook.sdk.attributionTracking"; + private static final String PUBLISH_ACTIVITY_PATH = "%s/activities"; + private static final String MOBILE_INSTALL_EVENT = "MOBILE_APP_INSTALL"; + private static final String ANALYTICS_EVENT = "event"; + private static final String AUTO_PUBLISH = "auto_publish"; + + private static final String APP_EVENT_PREFERENCES = "com.facebook.sdk.appEventPreferences"; + + private static final BlockingQueue DEFAULT_WORK_QUEUE = new LinkedBlockingQueue(10); + + private static final ThreadFactory DEFAULT_THREAD_FACTORY = new ThreadFactory() { + private final AtomicInteger counter = new AtomicInteger(0); + + public Thread newThread(Runnable runnable) { + return new Thread(runnable, "FacebookSdk #" + counter.incrementAndGet()); + } + }; + + /** + * loadDefaultsFromMetadata will attempt to load certain settings (e.g., application ID, client token) from + * metadata in the app's AndroidManifest.xml. The application ID will be read from this key. + */ + public static final String APPLICATION_ID_PROPERTY = "com.facebook.sdk.ApplicationId"; + /** + * loadDefaultsFromMetadata will attempt to load certain settings (e.g., application ID, client token) from + * metadata in the app's AndroidManifest.xml. The client token will be read from this key. + */ + public static final String CLIENT_TOKEN_PROPERTY = "com.facebook.sdk.ClientToken"; + + /** + * Certain logging behaviors are available for debugging beyond those that should be + * enabled in production. + * + * Returns the types of extended logging that are currently enabled. + * + * @return a set containing enabled logging behaviors + */ + public static final Set getLoggingBehaviors() { + synchronized (loggingBehaviors) { + return Collections.unmodifiableSet(new HashSet(loggingBehaviors)); + } + } + + /** + * Certain logging behaviors are available for debugging beyond those that should be + * enabled in production. + * + * Enables a particular extended logging in the sdk. + * + * @param behavior + * The LoggingBehavior to enable + */ + public static final void addLoggingBehavior(LoggingBehavior behavior) { + synchronized (loggingBehaviors) { + loggingBehaviors.add(behavior); + } + } + + /** + * Certain logging behaviors are available for debugging beyond those that should be + * enabled in production. + * + * Disables a particular extended logging behavior in the sdk. + * + * @param behavior + * The LoggingBehavior to disable + */ + public static final void removeLoggingBehavior(LoggingBehavior behavior) { + synchronized (loggingBehaviors) { + loggingBehaviors.remove(behavior); + } + } + + /** + * Certain logging behaviors are available for debugging beyond those that should be + * enabled in production. + * + * Disables all extended logging behaviors. + */ + public static final void clearLoggingBehaviors() { + synchronized (loggingBehaviors) { + loggingBehaviors.clear(); + } + } + + /** + * Certain logging behaviors are available for debugging beyond those that should be + * enabled in production. + * + * Checks if a particular extended logging behavior is enabled. + * + * @param behavior + * The LoggingBehavior to check + * @return whether behavior is enabled + */ + public static final boolean isLoggingBehaviorEnabled(LoggingBehavior behavior) { + synchronized (loggingBehaviors) { + return BuildConfig.DEBUG && loggingBehaviors.contains(behavior); + } + } + + /** + * Returns the Executor used by the SDK for non-AsyncTask background work. + * + * By default this uses AsyncTask Executor via reflection if the API level is high enough. + * Otherwise this creates a new Executor with defaults similar to those used in AsyncTask. + * + * @return an Executor used by the SDK. This will never be null. + */ + public static Executor getExecutor() { + synchronized (LOCK) { + if (Settings.executor == null) { + Executor executor = getAsyncTaskExecutor(); + if (executor == null) { + executor = new ThreadPoolExecutor(DEFAULT_CORE_POOL_SIZE, DEFAULT_MAXIMUM_POOL_SIZE, + DEFAULT_KEEP_ALIVE, TimeUnit.SECONDS, DEFAULT_WORK_QUEUE, DEFAULT_THREAD_FACTORY); + } + Settings.executor = executor; + } + } + return Settings.executor; + } + + /** + * Sets the Executor used by the SDK for non-AsyncTask background work. + * + * @param executor + * the Executor to use; must not be null. + */ + public static void setExecutor(Executor executor) { + Validate.notNull(executor, "executor"); + synchronized (LOCK) { + Settings.executor = executor; + } + } + + /** + * Gets the base Facebook domain to use when making Web requests; in production code this will always be + * "facebook.com". + * + * @return the Facebook domain + */ + public static String getFacebookDomain() { + return facebookDomain; + } + + /** + * Sets the base Facebook domain to use when making Web requests. This defaults to "facebook.com", but may + * be overridden to, e.g., "beta.facebook.com" to direct requests at a different domain. This method should + * never be called from production code. + * + * @param facebookDomain the base domain to use instead of "facebook.com" + */ + public static void setFacebookDomain(String facebookDomain) { + if (!BuildConfig.DEBUG) { + Log.w(TAG, "WARNING: Calling setFacebookDomain from non-DEBUG code."); + } + + Settings.facebookDomain = facebookDomain; + } + + private static Executor getAsyncTaskExecutor() { + Field executorField = null; + try { + executorField = AsyncTask.class.getField("THREAD_POOL_EXECUTOR"); + } catch (NoSuchFieldException e) { + return null; + } + + Object executorObject = null; + try { + executorObject = executorField.get(null); + } catch (IllegalAccessException e) { + return null; + } + + if (executorObject == null) { + return null; + } + + if (!(executorObject instanceof Executor)) { + return null; + } + + return (Executor) executorObject; + } + + /** + * Manually publish install attribution to the Facebook graph. Internally handles tracking repeat calls to prevent + * multiple installs being published to the graph. + * @param context the current Context + * @param applicationId the fb application being published. + * + * This method is deprecated. See {@link AppEventsLogger#activateApp(Context, String)} for more info. + */ + @Deprecated + public static void publishInstallAsync(final Context context, final String applicationId) { + publishInstallAsync(context, applicationId, null); + } + + /** + * Manually publish install attribution to the Facebook graph. Internally handles tracking repeat calls to prevent + * multiple installs being published to the graph. + * @param context the current Context + * @param applicationId the fb application being published. + * @param callback a callback to invoke with a Response object, carrying the server response, or an error. + * + * This method is deprecated. See {@link AppEventsLogger#activateApp(Context, String)} for more info. + */ + @Deprecated + public static void publishInstallAsync(final Context context, final String applicationId, + final Request.Callback callback) { + // grab the application context ahead of time, since we will return to the caller immediately. + final Context applicationContext = context.getApplicationContext(); + Settings.getExecutor().execute(new Runnable() { + @Override + public void run() { + final Response response = Settings.publishInstallAndWaitForResponse(applicationContext, applicationId); + if (callback != null) { + // invoke the callback on the main thread. + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + callback.onCompleted(response); + } + }); + } + } + }); + } + + /** + * Sets whether opening a Session should automatically publish install attribution to the Facebook graph. + * + * @param shouldAutoPublishInstall true to automatically publish, false to not + * + * This method is deprecated. See {@link AppEventsLogger#activateApp(Context, String)} for more info. + */ + @Deprecated + public static void setShouldAutoPublishInstall(boolean shouldAutoPublishInstall) { + Settings.shouldAutoPublishInstall = shouldAutoPublishInstall; + } + + /** + * Gets whether opening a Session should automatically publish install attribution to the Facebook graph. + * + * @return true to automatically publish, false to not + * + * This method is deprecated. See {@link AppEventsLogger#activateApp(Context, String)} for more info. + */ + @Deprecated + public static boolean getShouldAutoPublishInstall() { + return shouldAutoPublishInstall; + } + + /** + * Manually publish install attribution to the Facebook graph. Internally handles tracking repeat calls to prevent + * multiple installs being published to the graph. + * @param context the current Context + * @param applicationId the fb application being published. + * @return returns false on error. Applications should retry until true is returned. Safe to call again after + * true is returned. + * + * This method is deprecated. See {@link AppEventsLogger#activateApp(Context, String)} for more info. + */ + @Deprecated + public static boolean publishInstallAndWait(final Context context, final String applicationId) { + Response response = publishInstallAndWaitForResponse(context, applicationId); + return response != null && response.getError() == null; + } + + /** + * Manually publish install attribution to the Facebook graph. Internally handles caching repeat calls to prevent + * multiple installs being published to the graph. + * @param context the current Context + * @param applicationId the fb application being published. + * @return returns a Response object, carrying the server response, or an error. + * + * This method is deprecated. See {@link AppEventsLogger#activateApp(Context, String)} for more info. + */ + @Deprecated + public static Response publishInstallAndWaitForResponse(final Context context, final String applicationId) { + return publishInstallAndWaitForResponse(context, applicationId, false); + } + + static Response publishInstallAndWaitForResponse( + final Context context, + final String applicationId, + final boolean isAutoPublish) { + try { + if (context == null || applicationId == null) { + throw new IllegalArgumentException("Both context and applicationId must be non-null"); + } + AttributionIdentifiers identifiers = AttributionIdentifiers.getAttributionIdentifiers(context); + SharedPreferences preferences = context.getSharedPreferences(ATTRIBUTION_PREFERENCES, Context.MODE_PRIVATE); + String pingKey = applicationId+"ping"; + String jsonKey = applicationId+"json"; + long lastPing = preferences.getLong(pingKey, 0); + String lastResponseJSON = preferences.getString(jsonKey, null); + + // prevent auto publish from occurring if we have an explicit call. + if (!isAutoPublish) { + setShouldAutoPublishInstall(false); + } + + GraphObject publishParams = GraphObject.Factory.create(); + publishParams.setProperty(ANALYTICS_EVENT, MOBILE_INSTALL_EVENT); + + Utility.setAppEventAttributionParameters(publishParams, + identifiers, + Utility.getHashedDeviceAndAppID(context, applicationId), + getLimitEventAndDataUsage(context)); + publishParams.setProperty(AUTO_PUBLISH, isAutoPublish); + publishParams.setProperty("application_package_name", context.getPackageName()); + + String publishUrl = String.format(PUBLISH_ACTIVITY_PATH, applicationId); + Request publishRequest = Request.newPostRequest(null, publishUrl, publishParams, null); + + if (lastPing != 0) { + GraphObject graphObject = null; + try { + if (lastResponseJSON != null) { + graphObject = GraphObject.Factory.create(new JSONObject(lastResponseJSON)); + } + } + catch (JSONException je) { + // return the default graph object if there is any problem reading the data. + } + if (graphObject == null) { + return Response.createResponsesFromString("true", null, new RequestBatch(publishRequest), true).get(0); + } else { + return new Response(null, null, null, graphObject, true); + } + } else if (identifiers.getAndroidAdvertiserId() == null && identifiers.getAttributionId() == null) { + throw new FacebookException("No attribution id available to send to server."); + } else { + if (!Utility.queryAppSettings(applicationId, false).supportsAttribution()) { + throw new FacebookException("Install attribution has been disabled on the server."); + } + + Response publishResponse = publishRequest.executeAndWait(); + + // denote success since no error threw from the post. + SharedPreferences.Editor editor = preferences.edit(); + lastPing = System.currentTimeMillis(); + editor.putLong(pingKey, lastPing); + + // if we got an object response back, cache the string of the JSON. + if (publishResponse.getGraphObject() != null && + publishResponse.getGraphObject().getInnerJSONObject() != null) { + editor.putString(jsonKey, publishResponse.getGraphObject().getInnerJSONObject().toString()); + } + editor.commit(); + + return publishResponse; + } + } catch (Exception e) { + // if there was an error, fall through to the failure case. + Utility.logd("Facebook-publish", e); + return new Response(null, null, new FacebookRequestError(null, e)); + } + } + + /** + * Acquire the current attribution id from the facebook app. + * @return returns null if the facebook app is not present on the phone. + */ + public static String getAttributionId(ContentResolver contentResolver) { + try { + String [] projection = {ATTRIBUTION_ID_COLUMN_NAME}; + Cursor c = contentResolver.query(ATTRIBUTION_ID_CONTENT_URI, projection, null, null, null); + if (c == null || !c.moveToFirst()) { + return null; + } + String attributionId = c.getString(c.getColumnIndex(ATTRIBUTION_ID_COLUMN_NAME)); + c.close(); + return attributionId; + } catch (Exception e) { + Log.d(TAG, "Caught unexpected exception in getAttributionId(): " + e.toString()); + return null; + } + } + + /** + * Gets the application version to the provided string. + * @return application version set via setAppVersion. + */ + public static String getAppVersion() { + return appVersion; + } + + /** + * Sets the application version to the provided string. AppEventsLogger.logEvent calls logs its event with the + * current app version, and App Insights allows breakdown of events by app version. + * + * @param appVersion The version identifier of the Android app that events are being logged through. + * Enables analysis and breakdown of logged events by app version. + */ + public static void setAppVersion(String appVersion) { + Settings.appVersion = appVersion; + } + + /** + * Gets the current version of the Facebook SDK for Android as a string. + * + * @return the current version of the SDK + */ + public static String getSdkVersion() { + return FacebookSdkVersion.BUILD; + } + + /** + * Gets whether data such as that generated through AppEventsLogger and sent to Facebook should be restricted from + * being used for purposes other than analytics and conversions, such as for targeting ads to this user. Defaults + * to false. This value is stored on the device and persists across app launches. + * + * @param context Used to read the value. + */ + public static boolean getLimitEventAndDataUsage(Context context) { + SharedPreferences preferences = context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE); + return preferences.getBoolean("limitEventUsage", false); + } + + /** + * Sets whether data such as that generated through AppEventsLogger and sent to Facebook should be restricted from + * being used for purposes other than analytics and conversions, such as for targeting ads to this user. Defaults + * to false. This value is stored on the device and persists across app launches. Changes to this setting will + * apply to app events currently queued to be flushed. + * + * @param context Used to persist this value across app runs. + */ + public static void setLimitEventAndDataUsage(Context context, boolean limitEventUsage) { + SharedPreferences preferences = context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean("limitEventUsage", limitEventUsage); + editor.commit(); + } + + /** + * Gets the threshold used to report progress on requests. + */ + public static long getOnProgressThreshold() { + return onProgressThreshold.get(); + } + + /** + * Sets the threshold used to report progress on requests. Note that the value will be read when the + * request is started and can not be changed during a request (or batch) execution. + * + * @param threshold The number of bytes progressed to force a callback. + */ + public static void setOnProgressThreshold(long threshold) { + onProgressThreshold.set(threshold); + } + + /** + * Gets whether the SDK is running in Platform Compatibility mode (i.e. making calls to v1.0 endpoints by default) + * The default is false. + * + * @return the value + */ + public static boolean getPlatformCompatibilityEnabled() { + return platformCompatibilityEnabled; + } + + /** + * Sets whether the SDK is running in Platform Compatibility mode (i.e. making calls to v1.0 endpoints by default) + * The default is false. This is provided for apps that have strong reason not to take advantage of new + * capabilities in version 2.0+ of the API. + * + * @param platformCompatibilityEnabled whether to set Legacy Graph API mode + */ + public static void setPlatformCompatibilityEnabled(boolean platformCompatibilityEnabled) { + Settings.platformCompatibilityEnabled = platformCompatibilityEnabled; + } + + /** + * Loads default values for certain settings from an application's AndroidManifest.xml metadata, if possible. + * If values have been explicitly set for a particular setting, they will not be overwritten. The following + * settings are currently loaded from metadata: APPLICATION_ID_PROPERTY, CLIENT_TOKEN_PROPERTY + * @param context the Context to use for loading metadata + */ + public static void loadDefaultsFromMetadata(Context context) { + defaultsLoaded = true; + + if (context == null) { + return; + } + + ApplicationInfo ai = null; + try { + ai = context.getPackageManager().getApplicationInfo( + context.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + return; + } + + if (ai == null || ai.metaData == null) { + return; + } + + if (applicationId == null) { + applicationId = ai.metaData.getString(APPLICATION_ID_PROPERTY); + } + if (appClientToken == null) { + appClientToken = ai.metaData.getString(CLIENT_TOKEN_PROPERTY); + } + } + + static void loadDefaultsFromMetadataIfNeeded(Context context) { + if (!defaultsLoaded) { + loadDefaultsFromMetadata(context); + } + } + + /** + * Gets the Facebook application ID for the current app. This will be null unless explicitly set or unless + * loadDefaultsFromMetadata has been called. + * @return the application ID + */ + public static String getApplicationId() { + return applicationId; + } + + /** + * Sets the Facebook application ID for the current app. + * @param applicationId the application ID + */ + public static void setApplicationId(String applicationId) { + Settings.applicationId = applicationId; + } + + /** + * Gets the client token for the current app. This will be null unless explicitly set or unless + * loadDefaultsFromMetadata has been called. + * @return the client token + */ + public static String getClientToken() { + return appClientToken; + } + + /** + * Sets the Facebook client token for the current app. + * @param clientToken the client token + */ + public static void setClientToken(String clientToken) { + appClientToken = clientToken; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/SharedPreferencesTokenCachingStrategy.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SharedPreferencesTokenCachingStrategy.java new file mode 100644 index 0000000000..0286d3c1f4 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/SharedPreferencesTokenCachingStrategy.java @@ -0,0 +1,403 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import com.facebook.internal.Logger; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/* + *

+ * An implementation of {@link TokenCachingStrategy TokenCachingStrategy} that uses Android SharedPreferences + * to persist information. + *

+ *

+ * The data to be cached is passed in via a Bundle. Only non-null key-value-pairs where + * the value is one of the following types (or an array of the same) are persisted: + * boolean, byte, int, long, float, double, char. In addition, String and List + * are also supported. + *

+ */ +public class SharedPreferencesTokenCachingStrategy extends TokenCachingStrategy { + + private static final String DEFAULT_CACHE_KEY = "com.facebook.SharedPreferencesTokenCachingStrategy.DEFAULT_KEY"; + private static final String TAG = SharedPreferencesTokenCachingStrategy.class.getSimpleName(); + + private static final String JSON_VALUE_TYPE = "valueType"; + private static final String JSON_VALUE = "value"; + private static final String JSON_VALUE_ENUM_TYPE = "enumType"; + + private static final String TYPE_BOOLEAN = "bool"; + private static final String TYPE_BOOLEAN_ARRAY = "bool[]"; + private static final String TYPE_BYTE = "byte"; + private static final String TYPE_BYTE_ARRAY = "byte[]"; + private static final String TYPE_SHORT = "short"; + private static final String TYPE_SHORT_ARRAY = "short[]"; + private static final String TYPE_INTEGER = "int"; + private static final String TYPE_INTEGER_ARRAY = "int[]"; + private static final String TYPE_LONG = "long"; + private static final String TYPE_LONG_ARRAY = "long[]"; + private static final String TYPE_FLOAT = "float"; + private static final String TYPE_FLOAT_ARRAY = "float[]"; + private static final String TYPE_DOUBLE = "double"; + private static final String TYPE_DOUBLE_ARRAY = "double[]"; + private static final String TYPE_CHAR = "char"; + private static final String TYPE_CHAR_ARRAY = "char[]"; + private static final String TYPE_STRING = "string"; + private static final String TYPE_STRING_LIST = "stringList"; + private static final String TYPE_ENUM = "enum"; + + private String cacheKey; + private SharedPreferences cache; + + /** + * Creates a default {@link SharedPreferencesTokenCachingStrategy SharedPreferencesTokenCachingStrategy} + * instance that provides access to a single set of token information. + * + * @param context + * The Context object to use to get the SharedPreferences object. + * + * @throws NullPointerException if the passed in Context is null + */ + public SharedPreferencesTokenCachingStrategy(Context context) { + this(context, null); + } + + /** + * Creates a {@link SharedPreferencesTokenCachingStrategy SharedPreferencesTokenCachingStrategy} instance + * that is distinct for the passed in cacheKey. + * + * @param context + * The Context object to use to get the SharedPreferences object. + * + * @param cacheKey + * Identifies a distinct set of token information. + * + * @throws NullPointerException if the passed in Context is null + */ + public SharedPreferencesTokenCachingStrategy(Context context, String cacheKey) { + Validate.notNull(context, "context"); + + this.cacheKey = Utility.isNullOrEmpty(cacheKey) ? DEFAULT_CACHE_KEY : cacheKey; + + // If the application context is available, use that. However, if it isn't + // available (possibly because of a context that was created manually), use + // the passed in context directly. + Context applicationContext = context.getApplicationContext(); + context = applicationContext != null ? applicationContext : context; + + this.cache = context.getSharedPreferences( + this.cacheKey, + Context.MODE_PRIVATE); + } + + /** + * Returns a Bundle that contains the information stored in this cache + * + * @return A Bundle with the information contained in this cache + */ + public Bundle load() { + Bundle settings = new Bundle(); + + Map allCachedEntries = cache.getAll(); + + for (String key : allCachedEntries.keySet()) { + try { + deserializeKey(key, settings); + } catch (JSONException e) { + // Error in the cache. So consider it corrupted and return null + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, + "Error reading cached value for key: '" + key + "' -- " + e); + return null; + } + } + + return settings; + } + + /** + * Persists all supported data types present in the passed in Bundle, to the + * cache + * + * @param bundle + * The Bundle containing information to be cached + */ + public void save(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + + SharedPreferences.Editor editor = cache.edit(); + + for (String key : bundle.keySet()) { + try { + serializeKey(key, bundle, editor); + } catch (JSONException e) { + // Error in the bundle. Don't store a partial cache. + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error processing value for key: '" + key + "' -- " + e); + + // Bypass the commit and just return. This cancels the entire edit transaction + return; + } + } + + boolean successfulCommit = editor.commit(); + if (!successfulCommit) { + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "SharedPreferences.Editor.commit() was not successful"); + } + } + + /** + * Clears out all token information stored in this cache. + */ + public void clear() { + cache.edit().clear().commit(); + } + + private void serializeKey(String key, Bundle bundle, SharedPreferences.Editor editor) + throws JSONException { + Object value = bundle.get(key); + if (value == null) { + // Cannot serialize null values. + return; + } + + String supportedType = null; + JSONArray jsonArray = null; + JSONObject json = new JSONObject(); + + if (value instanceof Byte) { + supportedType = TYPE_BYTE; + json.put(JSON_VALUE, ((Byte)value).intValue()); + } else if (value instanceof Short) { + supportedType = TYPE_SHORT; + json.put(JSON_VALUE, ((Short)value).intValue()); + } else if (value instanceof Integer) { + supportedType = TYPE_INTEGER; + json.put(JSON_VALUE, ((Integer)value).intValue()); + } else if (value instanceof Long) { + supportedType = TYPE_LONG; + json.put(JSON_VALUE, ((Long)value).longValue()); + } else if (value instanceof Float) { + supportedType = TYPE_FLOAT; + json.put(JSON_VALUE, ((Float)value).doubleValue()); + } else if (value instanceof Double) { + supportedType = TYPE_DOUBLE; + json.put(JSON_VALUE, ((Double)value).doubleValue()); + } else if (value instanceof Boolean) { + supportedType = TYPE_BOOLEAN; + json.put(JSON_VALUE, ((Boolean)value).booleanValue()); + } else if (value instanceof Character) { + supportedType = TYPE_CHAR; + json.put(JSON_VALUE, value.toString()); + } else if (value instanceof String) { + supportedType = TYPE_STRING; + json.put(JSON_VALUE, (String)value); + } else if (value instanceof Enum) { + supportedType = TYPE_ENUM; + json.put(JSON_VALUE, value.toString()); + json.put(JSON_VALUE_ENUM_TYPE, value.getClass().getName()); + } else { + // Optimistically create a JSONArray. If not an array type, we can null + // it out later + jsonArray = new JSONArray(); + if (value instanceof byte[]) { + supportedType = TYPE_BYTE_ARRAY; + for (byte v : (byte[])value) { + jsonArray.put((int)v); + } + } else if (value instanceof short[]) { + supportedType = TYPE_SHORT_ARRAY; + for (short v : (short[])value) { + jsonArray.put((int)v); + } + } else if (value instanceof int[]) { + supportedType = TYPE_INTEGER_ARRAY; + for (int v : (int[])value) { + jsonArray.put(v); + } + } else if (value instanceof long[]) { + supportedType = TYPE_LONG_ARRAY; + for (long v : (long[])value) { + jsonArray.put(v); + } + } else if (value instanceof float[]) { + supportedType = TYPE_FLOAT_ARRAY; + for (float v : (float[])value) { + jsonArray.put((double)v); + } + } else if (value instanceof double[]) { + supportedType = TYPE_DOUBLE_ARRAY; + for (double v : (double[])value) { + jsonArray.put(v); + } + } else if (value instanceof boolean[]) { + supportedType = TYPE_BOOLEAN_ARRAY; + for (boolean v : (boolean[])value) { + jsonArray.put(v); + } + } else if (value instanceof char[]) { + supportedType = TYPE_CHAR_ARRAY; + for (char v : (char[])value) { + jsonArray.put(String.valueOf(v)); + } + } else if (value instanceof List) { + supportedType = TYPE_STRING_LIST; + @SuppressWarnings("unchecked") + List stringList = (List)value; + for (String v : stringList) { + jsonArray.put((v == null) ? JSONObject.NULL : v); + } + } else { + // Unsupported type. Clear out the array as a precaution even though + // it is redundant with the null supportedType. + jsonArray = null; + } + } + + if (supportedType != null) { + json.put(JSON_VALUE_TYPE, supportedType); + if (jsonArray != null) { + // If we have an array, it has already been converted to JSON. So use + // that instead. + json.putOpt(JSON_VALUE, jsonArray); + } + + String jsonString = json.toString(); + editor.putString(key, jsonString); + } + } + + private void deserializeKey(String key, Bundle bundle) + throws JSONException { + String jsonString = cache.getString(key, "{}"); + JSONObject json = new JSONObject(jsonString); + + String valueType = json.getString(JSON_VALUE_TYPE); + + if (valueType.equals(TYPE_BOOLEAN)) { + bundle.putBoolean(key, json.getBoolean(JSON_VALUE)); + } else if (valueType.equals(TYPE_BOOLEAN_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + boolean[] array = new boolean[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = jsonArray.getBoolean(i); + } + bundle.putBooleanArray(key, array); + } else if (valueType.equals(TYPE_BYTE)) { + bundle.putByte(key, (byte)json.getInt(JSON_VALUE)); + } else if (valueType.equals(TYPE_BYTE_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + byte[] array = new byte[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = (byte)jsonArray.getInt(i); + } + bundle.putByteArray(key, array); + } else if (valueType.equals(TYPE_SHORT)) { + bundle.putShort(key, (short)json.getInt(JSON_VALUE)); + } else if (valueType.equals(TYPE_SHORT_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + short[] array = new short[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = (short)jsonArray.getInt(i); + } + bundle.putShortArray(key, array); + } else if (valueType.equals(TYPE_INTEGER)) { + bundle.putInt(key, json.getInt(JSON_VALUE)); + } else if (valueType.equals(TYPE_INTEGER_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + int[] array = new int[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = jsonArray.getInt(i); + } + bundle.putIntArray(key, array); + } else if (valueType.equals(TYPE_LONG)) { + bundle.putLong(key, json.getLong(JSON_VALUE)); + } else if (valueType.equals(TYPE_LONG_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + long[] array = new long[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = jsonArray.getLong(i); + } + bundle.putLongArray(key, array); + } else if (valueType.equals(TYPE_FLOAT)) { + bundle.putFloat(key, (float)json.getDouble(JSON_VALUE)); + } else if (valueType.equals(TYPE_FLOAT_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + float[] array = new float[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = (float)jsonArray.getDouble(i); + } + bundle.putFloatArray(key, array); + } else if (valueType.equals(TYPE_DOUBLE)) { + bundle.putDouble(key, json.getDouble(JSON_VALUE)); + } else if (valueType.equals(TYPE_DOUBLE_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + double[] array = new double[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + array[i] = jsonArray.getDouble(i); + } + bundle.putDoubleArray(key, array); + } else if (valueType.equals(TYPE_CHAR)) { + String charString = json.getString(JSON_VALUE); + if (charString != null && charString.length() == 1) { + bundle.putChar(key, charString.charAt(0)); + } + } else if (valueType.equals(TYPE_CHAR_ARRAY)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + char[] array = new char[jsonArray.length()]; + for (int i = 0; i < array.length; i++) { + String charString = jsonArray.getString(i); + if (charString != null && charString.length() == 1) { + array[i] = charString.charAt(0); + } + } + bundle.putCharArray(key, array); + } else if (valueType.equals(TYPE_STRING)) { + bundle.putString(key, json.getString(JSON_VALUE)); + } else if (valueType.equals(TYPE_STRING_LIST)) { + JSONArray jsonArray = json.getJSONArray(JSON_VALUE); + int numStrings = jsonArray.length(); + ArrayList stringList = new ArrayList(numStrings); + for (int i = 0; i < numStrings; i++) { + Object jsonStringValue = jsonArray.get(i); + stringList.add(i, jsonStringValue == JSONObject.NULL ? null : (String)jsonStringValue); + } + bundle.putStringArrayList(key, stringList); + } else if (valueType.equals(TYPE_ENUM)) { + try { + String enumType = json.getString(JSON_VALUE_ENUM_TYPE); + @SuppressWarnings({ "unchecked", "rawtypes" }) + Class enumClass = (Class) Class.forName(enumType); + @SuppressWarnings("unchecked") + Enum enumValue = Enum.valueOf(enumClass, json.getString(JSON_VALUE)); + bundle.putSerializable(key, enumValue); + } catch (ClassNotFoundException e) { + } catch (IllegalArgumentException e) { + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/TestSession.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/TestSession.java new file mode 100644 index 0000000000..821c1f95db --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/TestSession.java @@ -0,0 +1,528 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import com.facebook.model.GraphObject; +import com.facebook.model.GraphObjectList; +import com.facebook.internal.Logger; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.*; + +/** + * Implements an subclass of Session that knows about test users for a particular + * application. This should never be used from a real application, but may be useful + * for writing unit tests, etc. + *

+ * Facebook allows developers to create test accounts for testing their applications' + * Facebook integration (see https://developers.facebook.com/docs/test_users/). This class + * simplifies use of these accounts for writing unit tests. It is not designed for use in + * production application code. + *

+ * The main use case for this class is using {@link #createSessionWithPrivateUser(android.app.Activity, java.util.List)} + * or {@link #createSessionWithSharedUser(android.app.Activity, java.util.List)} + * to create a session for a test user. Two modes are supported. In "shared" mode, an attempt + * is made to find an existing test user that has the required permissions. If no such user is available, + * a new one is created with the required permissions. In "private" mode, designed for + * scenarios which require a new user in a known clean state, a new test user will always be + * created, and it will be automatically deleted when the TestSession is closed. The session + * obeys the same lifecycle as a regular Session, meaning it must be opened after creation before + * it can be used to make calls to the Facebook API. + *

+ * Prior to creating a TestSession, two static methods must be called to initialize the + * application ID and application Secret to be used for managing test users. These methods are + * {@link #setTestApplicationId(String)} and {@link #setTestApplicationSecret(String)}. + *

+ * Note that the shared test user functionality depends on a naming convention for the test users. + * It is important that any testing of functionality which will mutate the permissions for a + * test user NOT use a shared test user, or this scheme will break down. If a shared test user + * seems to be in an invalid state, it can be deleted manually via the Web interface at + * https://developers.facebook.com/apps/APP_ID/permissions?role=test+users. + */ +public class TestSession extends Session { + private static final long serialVersionUID = 1L; + + private enum Mode { + PRIVATE, SHARED + } + + private static final String LOG_TAG = Logger.LOG_TAG_BASE + "TestSession"; + + private static Map appTestAccounts; + private static String testApplicationSecret; + private static String testApplicationId; + + private final String sessionUniqueUserTag; + private final List requestedPermissions; + private final Mode mode; + private String testAccountId; + private String testAccountUserName; + + private boolean wasAskedToExtendAccessToken; + + TestSession(Activity activity, List permissions, TokenCachingStrategy tokenCachingStrategy, + String sessionUniqueUserTag, Mode mode) { + super(activity, TestSession.testApplicationId, tokenCachingStrategy); + + Validate.notNull(permissions, "permissions"); + + // Validate these as if they were arguments even though they are statics. + Validate.notNullOrEmpty(testApplicationId, "testApplicationId"); + Validate.notNullOrEmpty(testApplicationSecret, "testApplicationSecret"); + + this.sessionUniqueUserTag = sessionUniqueUserTag; + this.mode = mode; + this.requestedPermissions = permissions; + } + + /** + * Constructs a TestSession which creates a test user on open, and destroys the user on + * close; This method should not be used in application code -- but is useful for creating unit tests + * that use the Facebook SDK. + * + * @param activity the Activity to use for opening the session + * @param permissions list of strings containing permissions to request; nil will result in + * a common set of permissions (email, publish_actions) being requested + * @return a new TestSession that is in the CREATED state, ready to be opened + */ + public static TestSession createSessionWithPrivateUser(Activity activity, List permissions) { + return createTestSession(activity, permissions, Mode.PRIVATE, null); + } + + /** + * Constructs a TestSession which uses a shared test user with the right permissions, + * creating one if necessary on open (but not deleting it on close, so it can be re-used in later + * tests). + *

+ * This method should not be used in application code -- but is useful for creating unit tests + * that use the Facebook SDK. + * + * @param activity the Activity to use for opening the session + * @param permissions list of strings containing permissions to request; nil will result in + * a common set of permissions (email, publish_actions) being requested + * @return a new TestSession that is in the CREATED state, ready to be opened + */ + public static TestSession createSessionWithSharedUser(Activity activity, List permissions) { + return createSessionWithSharedUser(activity, permissions, null); + } + + /** + * Constructs a TestSession which uses a shared test user with the right permissions, + * creating one if necessary on open (but not deleting it on close, so it can be re-used in later + * tests). + *

+ * This method should not be used in application code -- but is useful for creating unit tests + * that use the Facebook SDK. + * + * @param activity the Activity to use for opening the session + * @param permissions list of strings containing permissions to request; nil will result in + * a common set of permissions (email, publish_actions) being requested + * @param sessionUniqueUserTag a string which will be used to make this user unique among other + * users with the same permissions. Useful for tests which require two or more users to interact + * with each other, and which therefore must have sessions associated with different users. + * @return a new TestSession that is in the CREATED state, ready to be opened + */ + public static TestSession createSessionWithSharedUser(Activity activity, List permissions, + String sessionUniqueUserTag) { + return createTestSession(activity, permissions, Mode.SHARED, sessionUniqueUserTag); + } + + /** + * Gets the Facebook Application ID for the application under test. + * + * @return the application ID + */ + public static synchronized String getTestApplicationId() { + return testApplicationId; + } + + /** + * Sets the Facebook Application ID for the application under test. This must be specified + * prior to creating a TestSession. + * + * @param applicationId the application ID + */ + public static synchronized void setTestApplicationId(String applicationId) { + if (testApplicationId != null && !testApplicationId.equals(applicationId)) { + throw new FacebookException("Can't have more than one test application ID"); + } + testApplicationId = applicationId; + } + + /** + * Gets the Facebook Application Secret for the application under test. + * + * @return the application secret + */ + public static synchronized String getTestApplicationSecret() { + return testApplicationSecret; + } + + /** + * Sets the Facebook Application Secret for the application under test. This must be specified + * prior to creating a TestSession. + * + * @param applicationSecret the application secret + */ + public static synchronized void setTestApplicationSecret(String applicationSecret) { + if (testApplicationSecret != null && !testApplicationSecret.equals(applicationSecret)) { + throw new FacebookException("Can't have more than one test application secret"); + } + testApplicationSecret = applicationSecret; + } + + /** + * Gets the ID of the test user that this TestSession is authenticated as. + * + * @return the Facebook user ID of the test user + */ + public final String getTestUserId() { + return testAccountId; + } + + /** + * Gets the name of the test user that this TestSession is authenticated as. + * + * @return the name of the test user + */ + public final String getTestUserName() { + return testAccountUserName; + } + + + private static synchronized TestSession createTestSession(Activity activity, List permissions, Mode mode, + String sessionUniqueUserTag) { + if (Utility.isNullOrEmpty(testApplicationId) || Utility.isNullOrEmpty(testApplicationSecret)) { + throw new FacebookException("Must provide app ID and secret"); + } + + if (Utility.isNullOrEmpty(permissions)) { + permissions = Arrays.asList("email", "publish_actions"); + } + + return new TestSession(activity, permissions, new TestTokenCachingStrategy(), sessionUniqueUserTag, + mode); + } + + private static synchronized void retrieveTestAccountsForAppIfNeeded() { + if (appTestAccounts != null) { + return; + } + + appTestAccounts = new HashMap(); + + // The data we need is split across two different FQL tables. We construct two queries, submit them + // together (the second one refers to the first one), then cross-reference the results. + + // Get the test accounts for this app. + String testAccountQuery = String.format("SELECT id,access_token FROM test_account WHERE app_id = %s", + testApplicationId); + // Get the user names for those accounts. + String userQuery = "SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)"; + + Bundle parameters = new Bundle(); + + // Build a JSON string that contains our queries and pass it as the 'q' parameter of the query. + JSONObject multiquery; + try { + multiquery = new JSONObject(); + multiquery.put("test_accounts", testAccountQuery); + multiquery.put("users", userQuery); + } catch (JSONException exception) { + throw new FacebookException(exception); + } + parameters.putString("q", multiquery.toString()); + + // We need to authenticate as this app. + parameters.putString("access_token", getAppAccessToken()); + + Request request = new Request(null, "fql", parameters, null); + Response response = request.executeAndWait(); + + if (response.getError() != null) { + throw response.getError().getException(); + } + + FqlResponse fqlResponse = response.getGraphObjectAs(FqlResponse.class); + + GraphObjectList fqlResults = fqlResponse.getData(); + if (fqlResults == null || fqlResults.size() != 2) { + throw new FacebookException("Unexpected number of results from FQL query"); + } + + // We get back two sets of results. The first is from the test_accounts query, the second from the users query. + Collection testAccounts = fqlResults.get(0).getFqlResultSet().castToListOf(TestAccount.class); + Collection userAccounts = fqlResults.get(1).getFqlResultSet().castToListOf(UserAccount.class); + + // Use both sets of results to populate our static array of accounts. + populateTestAccounts(testAccounts, userAccounts); + + return; + } + + private static synchronized void populateTestAccounts(Collection testAccounts, + Collection userAccounts) { + // We get different sets of data from each of these queries. We want to combine them into a single data + // structure. We have added a Name property to the TestAccount interface, even though we don't really get + // a name back from the service from that query. We stick the Name from the corresponding UserAccount in it. + for (TestAccount testAccount : testAccounts) { + storeTestAccount(testAccount); + } + + for (UserAccount userAccount : userAccounts) { + TestAccount testAccount = appTestAccounts.get(userAccount.getUid()); + if (testAccount != null) { + testAccount.setName(userAccount.getName()); + } + } + } + + private static synchronized void storeTestAccount(TestAccount testAccount) { + appTestAccounts.put(testAccount.getId(), testAccount); + } + + private static synchronized TestAccount findTestAccountMatchingIdentifier(String identifier) { + retrieveTestAccountsForAppIfNeeded(); + + for (TestAccount testAccount : appTestAccounts.values()) { + if (testAccount.getName().contains(identifier)) { + return testAccount; + } + } + return null; + } + + @Override + public final String toString() { + String superString = super.toString(); + + return new StringBuilder().append("{TestSession").append(" testUserId:").append(testAccountId) + .append(" ").append(superString).append("}").toString(); + } + + @Override + void authorize(AuthorizationRequest request) { + if (mode == Mode.PRIVATE) { + createTestAccountAndFinishAuth(); + } else { + findOrCreateSharedTestAccount(); + } + } + + @Override + void postStateChange(final SessionState oldState, final SessionState newState, final Exception error) { + // Make sure this doesn't get overwritten. + String id = testAccountId; + + super.postStateChange(oldState, newState, error); + + if (newState.isClosed() && id != null && mode == Mode.PRIVATE) { + deleteTestAccount(id, getAppAccessToken()); + } + } + + boolean getWasAskedToExtendAccessToken() { + return wasAskedToExtendAccessToken; + } + + void forceExtendAccessToken(boolean forceExtendAccessToken) { + AccessToken currentToken = getTokenInfo(); + setTokenInfo( + new AccessToken(currentToken.getToken(), new Date(), currentToken.getPermissions(), + currentToken.getDeclinedPermissions(), AccessTokenSource.TEST_USER, new Date(0))); + setLastAttemptedTokenExtendDate(new Date(0)); + } + + @Override + boolean shouldExtendAccessToken() { + boolean result = super.shouldExtendAccessToken(); + wasAskedToExtendAccessToken = false; + return result; + } + + @Override + void extendAccessToken() { + wasAskedToExtendAccessToken = true; + super.extendAccessToken(); + } + + void fakeTokenRefreshAttempt() { + setCurrentTokenRefreshRequest(new TokenRefreshRequest()); + } + + static final String getAppAccessToken() { + return testApplicationId + "|" + testApplicationSecret; + } + + private void findOrCreateSharedTestAccount() { + TestAccount testAccount = findTestAccountMatchingIdentifier(getSharedTestAccountIdentifier()); + if (testAccount != null) { + finishAuthWithTestAccount(testAccount); + } else { + createTestAccountAndFinishAuth(); + } + } + + private void finishAuthWithTestAccount(TestAccount testAccount) { + testAccountId = testAccount.getId(); + testAccountUserName = testAccount.getName(); + + AccessToken accessToken = AccessToken.createFromString(testAccount.getAccessToken(), requestedPermissions, + AccessTokenSource.TEST_USER); + finishAuthOrReauth(accessToken, null); + } + + private TestAccount createTestAccountAndFinishAuth() { + Bundle parameters = new Bundle(); + parameters.putString("installed", "true"); + parameters.putString("permissions", getPermissionsString()); + parameters.putString("access_token", getAppAccessToken()); + + // If we're in shared mode, we want to rename this user to encode its permissions, so we can find it later + // in another shared session. If we're in private mode, don't bother renaming it since we're just going to + // delete it at the end of the session. + if (mode == Mode.SHARED) { + parameters.putString("name", String.format("Shared %s Testuser", getSharedTestAccountIdentifier())); + } + + String graphPath = String.format("%s/accounts/test-users", testApplicationId); + Request createUserRequest = new Request(null, graphPath, parameters, HttpMethod.POST); + Response response = createUserRequest.executeAndWait(); + + FacebookRequestError error = response.getError(); + TestAccount testAccount = response.getGraphObjectAs(TestAccount.class); + if (error != null) { + finishAuthOrReauth(null, error.getException()); + return null; + } else { + assert testAccount != null; + + // If we are in shared mode, store this new account in the dictionary so we can re-use it later. + if (mode == Mode.SHARED) { + // Remember the new name we gave it, since we didn't get it back in the results of the create request. + testAccount.setName(parameters.getString("name")); + storeTestAccount(testAccount); + } + + finishAuthWithTestAccount(testAccount); + + return testAccount; + } + } + + private void deleteTestAccount(String testAccountId, String appAccessToken) { + Bundle parameters = new Bundle(); + parameters.putString("access_token", appAccessToken); + + Request request = new Request(null, testAccountId, parameters, HttpMethod.DELETE); + Response response = request.executeAndWait(); + + FacebookRequestError error = response.getError(); + GraphObject graphObject = response.getGraphObject(); + if (error != null) { + Log.w(LOG_TAG, String.format("Could not delete test account %s: %s", testAccountId, error.getException().toString())); + } else if (graphObject.getProperty(Response.NON_JSON_RESPONSE_PROPERTY) == (Boolean) false) { + Log.w(LOG_TAG, String.format("Could not delete test account %s: unknown reason", testAccountId)); + } + } + + private String getPermissionsString() { + return TextUtils.join(",", requestedPermissions); + } + + private String getSharedTestAccountIdentifier() { + // We use long even though hashes are ints to avoid sign issues. + long permissionsHash = getPermissionsString().hashCode() & 0xffffffffL; + long sessionTagHash = (sessionUniqueUserTag != null) ? sessionUniqueUserTag.hashCode() & 0xffffffffL : 0; + + long combinedHash = permissionsHash ^ sessionTagHash; + return validNameStringFromInteger(combinedHash); + } + + private String validNameStringFromInteger(long i) { + String s = Long.toString(i); + StringBuilder result = new StringBuilder("Perm"); + + // We know each character is a digit. Convert it into a letter 'a'-'j'. Avoid repeated characters + // that might make Facebook reject the name by converting every other repeated character into one + // 10 higher ('k'-'t'). + char lastChar = 0; + for (char c : s.toCharArray()) { + if (c == lastChar) { + c += 10; + } + result.append((char) (c + 'a' - '0')); + lastChar = c; + } + + return result.toString(); + } + + private interface TestAccount extends GraphObject { + String getId(); + + String getAccessToken(); + + // Note: We don't actually get Name from our FQL query. We fill it in by correlating with UserAccounts. + String getName(); + + void setName(String name); + } + + private interface UserAccount extends GraphObject { + String getUid(); + + String getName(); + + void setName(String name); + } + + private interface FqlResult extends GraphObject { + GraphObjectList getFqlResultSet(); + + } + + private interface FqlResponse extends GraphObject { + GraphObjectList getData(); + } + + private static final class TestTokenCachingStrategy extends TokenCachingStrategy { + private Bundle bundle; + + @Override + public Bundle load() { + return bundle; + } + + @Override + public void save(Bundle value) { + bundle = value; + } + + @Override + public void clear() { + bundle = null; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/TokenCachingStrategy.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/TokenCachingStrategy.java new file mode 100644 index 0000000000..c615be6408 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/TokenCachingStrategy.java @@ -0,0 +1,409 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.os.Bundle; +import com.facebook.internal.Validate; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + *

+ * A base class for implementations of a {@link Session Session} token cache. + *

+ *

+ * The Session constructor optionally takes a TokenCachingStrategy, from which it will + * attempt to load a cached token during construction. Also, whenever the + * Session updates its token, it will also save the token and associated state + * to the TokenCachingStrategy. + *

+ *

+ * This is the only mechanism supported for an Android service to use Session. + * The service can create a custom TokenCachingStrategy that returns the Session provided + * by an Activity through which the user logged in to Facebook. + *

+ */ +public abstract class TokenCachingStrategy { + /** + * The key used by Session to store the token value in the Bundle during + * load and save. + */ + public static final String TOKEN_KEY = "com.facebook.TokenCachingStrategy.Token"; + + /** + * The key used by Session to store the expiration date value in the Bundle + * during load and save. + */ + public static final String EXPIRATION_DATE_KEY = "com.facebook.TokenCachingStrategy.ExpirationDate"; + + /** + * The key used by Session to store the last refresh date value in the + * Bundle during load and save. + */ + public static final String LAST_REFRESH_DATE_KEY = "com.facebook.TokenCachingStrategy.LastRefreshDate"; + + /** + * The key used by Session to store the user's id value in the Bundle during + * load and save. + */ + public static final String USER_FBID_KEY = "com.facebook.TokenCachingStrategy.UserFBID"; + + /** + * The key used by Session to store an enum indicating the source of the token + * in the Bundle during load and save. + */ + public static final String TOKEN_SOURCE_KEY = "com.facebook.TokenCachingStrategy.AccessTokenSource"; + + /** + * The key used by Session to store the list of permissions granted by the + * token in the Bundle during load and save. + */ + public static final String PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.Permissions"; + + /** + * The key used by Session to store the list of permissions declined by the user in the token in the Bundle + * during load and save. + */ + public static final String DECLINED_PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.DeclinedPermissions"; + + private static final long INVALID_BUNDLE_MILLISECONDS = Long.MIN_VALUE; + private static final String IS_SSO_KEY = "com.facebook.TokenCachingStrategy.IsSSO"; + + /** + * Called during Session construction to get the token state. Typically this + * is loaded from a persistent store that was previously initialized via + * save. The caller may choose to keep a reference to the returned Bundle + * indefinitely. Therefore the TokenCachingStrategy should not store the returned Bundle + * and should return a new Bundle on every call to this method. + * + * @return A Bundle that represents the token state that was loaded. + */ + public abstract Bundle load(); + + /** + * Called when a Session updates its token. This is passed a Bundle of + * values that should be stored durably for the purpose of being returned + * from a later call to load. Some implementations may choose to store + * bundle beyond the scope of this call, so the caller should keep no + * references to the bundle to ensure that it is not modified later. + * + * @param bundle + * A Bundle that represents the token state to be saved. + */ + public abstract void save(Bundle bundle); + + /** + * Called when a Session learns its token is no longer valid or during a + * call to {@link Session#closeAndClearTokenInformation + * closeAndClearTokenInformation} to clear the durable state associated with + * the token. + */ + public abstract void clear(); + + /** + * Returns a boolean indicating whether a Bundle contains properties that + * could be a valid saved token. + * + * @param bundle + * A Bundle to check for token information. + * @return a boolean indicating whether a Bundle contains properties that + * could be a valid saved token. + */ + public static boolean hasTokenInformation(Bundle bundle) { + if (bundle == null) { + return false; + } + + String token = bundle.getString(TOKEN_KEY); + if ((token == null) || (token.length() == 0)) { + return false; + } + + long expiresMilliseconds = bundle.getLong(EXPIRATION_DATE_KEY, 0L); + if (expiresMilliseconds == 0L) { + return false; + } + + return true; + } + + /** + * Gets the cached token value from a Bundle. + * + * @param bundle + * A Bundle in which the token value was stored. + * @return the cached token value, or null. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static String getToken(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return bundle.getString(TOKEN_KEY); + } + + /** + * Puts the token value into a Bundle. + * + * @param bundle + * A Bundle in which the token value should be stored. + * @param value + * The String representing the token value, or null. + * + * @throws NullPointerException if the passed in Bundle or token value are null + */ + public static void putToken(Bundle bundle, String value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + bundle.putString(TOKEN_KEY, value); + } + + /** + * Gets the cached expiration date from a Bundle. + * + * @param bundle + * A Bundle in which the expiration date was stored. + * @return the cached expiration date, or null. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static Date getExpirationDate(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return getDate(bundle, EXPIRATION_DATE_KEY); + } + + /** + * Puts the expiration date into a Bundle. + * + * @param bundle + * A Bundle in which the expiration date should be stored. + * @param value + * The Date representing the expiration date. + * + * @throws NullPointerException if the passed in Bundle or date value are null + */ + public static void putExpirationDate(Bundle bundle, Date value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + putDate(bundle, EXPIRATION_DATE_KEY, value); + } + + /** + * Gets the cached expiration date from a Bundle. + * + * @param bundle + * A Bundle in which the expiration date was stored. + * @return the long representing the cached expiration date in milliseconds + * since the epoch, or 0. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static long getExpirationMilliseconds(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return bundle.getLong(EXPIRATION_DATE_KEY); + } + + /** + * Puts the expiration date into a Bundle. + * + * @param bundle + * A Bundle in which the expiration date should be stored. + * @param value + * The long representing the expiration date in milliseconds + * since the epoch. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static void putExpirationMilliseconds(Bundle bundle, long value) { + Validate.notNull(bundle, "bundle"); + bundle.putLong(EXPIRATION_DATE_KEY, value); + } + + /** + * Gets the cached list of permissions from a Bundle. + * + * @param bundle + * A Bundle in which the list of permissions was stored. + * @return the cached list of permissions. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static List getPermissions(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return bundle.getStringArrayList(PERMISSIONS_KEY); + } + + /** + * Puts the list of permissions into a Bundle. + * + * @param bundle + * A Bundle in which the list of permissions should be stored. + * @param value + * The List<String> representing the list of permissions, + * or null. + * + * @throws NullPointerException if the passed in Bundle or permissions list are null + */ + public static void putPermissions(Bundle bundle, List value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + + ArrayList arrayList; + if (value instanceof ArrayList) { + arrayList = (ArrayList) value; + } else { + arrayList = new ArrayList(value); + } + bundle.putStringArrayList(PERMISSIONS_KEY, arrayList); + } + + /** + * Puts the list of declined permissions into a Bundle. + * + * @param bundle + * A Bundle in which the list of permissions should be stored. + * @param value + * The List<String> representing the list of permissions, + * or null. + * + * @throws NullPointerException if the passed in Bundle or permissions list are null + */ + public static void putDeclinedPermissions(Bundle bundle, List value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + + ArrayList arrayList; + if (value instanceof ArrayList) { + arrayList = (ArrayList) value; + } else { + arrayList = new ArrayList(value); + } + bundle.putStringArrayList(DECLINED_PERMISSIONS_KEY, arrayList); + } + + + /** + * Gets the cached enum indicating the source of the token from the Bundle. + * + * @param bundle + * A Bundle in which the enum was stored. + * @return enum indicating the source of the token + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static AccessTokenSource getSource(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + if (bundle.containsKey(TokenCachingStrategy.TOKEN_SOURCE_KEY)) { + return (AccessTokenSource) bundle.getSerializable(TokenCachingStrategy.TOKEN_SOURCE_KEY); + } else { + boolean isSSO = bundle.getBoolean(TokenCachingStrategy.IS_SSO_KEY); + return isSSO ? AccessTokenSource.FACEBOOK_APPLICATION_WEB : AccessTokenSource.WEB_VIEW; + } + } + /** + * Puts the enum indicating the source of the token into a Bundle. + * + * @param bundle + * A Bundle in which the enum should be stored. + * @param value + * enum indicating the source of the token + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static void putSource(Bundle bundle, AccessTokenSource value) { + Validate.notNull(bundle, "bundle"); + bundle.putSerializable(TOKEN_SOURCE_KEY, value); + } + + /** + * Gets the cached last refresh date from a Bundle. + * + * @param bundle + * A Bundle in which the last refresh date was stored. + * @return the cached last refresh Date, or null. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static Date getLastRefreshDate(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return getDate(bundle, LAST_REFRESH_DATE_KEY); + } + + /** + * Puts the last refresh date into a Bundle. + * + * @param bundle + * A Bundle in which the last refresh date should be stored. + * @param value + * The Date representing the last refresh date, or null. + * + * @throws NullPointerException if the passed in Bundle or date value are null + */ + public static void putLastRefreshDate(Bundle bundle, Date value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + putDate(bundle, LAST_REFRESH_DATE_KEY, value); + } + + /** + * Gets the cached last refresh date from a Bundle. + * + * @param bundle + * A Bundle in which the last refresh date was stored. + * @return the cached last refresh date in milliseconds since the epoch. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static long getLastRefreshMilliseconds(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return bundle.getLong(LAST_REFRESH_DATE_KEY); + } + + /** + * Puts the last refresh date into a Bundle. + * + * @param bundle + * A Bundle in which the last refresh date should be stored. + * @param value + * The long representing the last refresh date in milliseconds + * since the epoch. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static void putLastRefreshMilliseconds(Bundle bundle, long value) { + Validate.notNull(bundle, "bundle"); + bundle.putLong(LAST_REFRESH_DATE_KEY, value); + } + + static Date getDate(Bundle bundle, String key) { + if (bundle == null) { + return null; + } + + long n = bundle.getLong(key, INVALID_BUNDLE_MILLISECONDS); + if (n == INVALID_BUNDLE_MILLISECONDS) { + return null; + } + + return new Date(n); + } + + static void putDate(Bundle bundle, String key, Date date) { + bundle.putLong(key, date.getTime()); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/UiLifecycleHelper.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/UiLifecycleHelper.java new file mode 100644 index 0000000000..7b68c16fa5 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/UiLifecycleHelper.java @@ -0,0 +1,312 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import com.facebook.internal.NativeProtocol; +import com.facebook.widget.FacebookDialog; + +import java.util.UUID; + +/** + * This class helps to create, automatically open (if applicable), save, and + * restore the Active Session in a way that is similar to Android UI lifecycles. + *

+ * When using this class, clients MUST call all the public methods from the + * respective methods in either an Activity or Fragment. Failure to call all the + * methods can result in improperly initialized or uninitialized Sessions. + *

+ * This class should also be used by Activities that will be displaying native dialogs + * provided by the Facebook application, in order to handle processing of the activity + * results generated by those dialogs. + */ +public class UiLifecycleHelper { + private static final String DIALOG_CALL_BUNDLE_SAVE_KEY = + "com.facebook.UiLifecycleHelper.pendingFacebookDialogCallKey"; + + private final static String ACTIVITY_NULL_MESSAGE = "activity cannot be null"; + + private final Activity activity; + private final Session.StatusCallback callback; + private final BroadcastReceiver receiver; + private final LocalBroadcastManager broadcastManager; + // Members related to handling FacebookDialog calls + private FacebookDialog.PendingCall pendingFacebookDialogCall; + private AppEventsLogger appEventsLogger; + + /** + * Creates a new UiLifecycleHelper. + * + * @param activity the Activity associated with the helper. If calling from a Fragment, + * use {@link android.support.v4.app.Fragment#getActivity()} + * @param callback the callback for Session status changes, can be null + */ + public UiLifecycleHelper(Activity activity, Session.StatusCallback callback) { + if (activity == null) { + throw new IllegalArgumentException(ACTIVITY_NULL_MESSAGE); + } + this.activity = activity; + this.callback = callback; + this.receiver = new ActiveSessionBroadcastReceiver(); + this.broadcastManager = LocalBroadcastManager.getInstance(activity); + + // Make sure we've loaded default settings if we haven't already. + Settings.loadDefaultsFromMetadataIfNeeded(activity); + } + + /** + * To be called from an Activity or Fragment's onCreate method. + * + * @param savedInstanceState the previously saved state + */ + public void onCreate(Bundle savedInstanceState) { + Session session = Session.getActiveSession(); + if (session == null) { + if (savedInstanceState != null) { + session = Session.restoreSession(activity, null, callback, savedInstanceState); + } + if (session == null) { + session = new Session(activity); + } + Session.setActiveSession(session); + } + if (savedInstanceState != null) { + pendingFacebookDialogCall = savedInstanceState.getParcelable(DIALOG_CALL_BUNDLE_SAVE_KEY); + } + } + + /** + * To be called from an Activity or Fragment's onResume method. + */ + public void onResume() { + Session session = Session.getActiveSession(); + if (session != null) { + if (callback != null) { + session.addCallback(callback); + } + if (SessionState.CREATED_TOKEN_LOADED.equals(session.getState())) { + session.openForRead(null); + } + } + + // add the broadcast receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(Session.ACTION_ACTIVE_SESSION_SET); + filter.addAction(Session.ACTION_ACTIVE_SESSION_UNSET); + + // Add a broadcast receiver to listen to when the active Session + // is set or unset, and add/remove our callback as appropriate + broadcastManager.registerReceiver(receiver, filter); + } + + /** + * To be called from an Activity or Fragment's onActivityResult method. + * + * @param requestCode the request code + * @param resultCode the result code + * @param data the result data + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + onActivityResult(requestCode, resultCode, data, null); + } + + /** + * To be called from an Activity or Fragment's onActivityResult method, when the results of a FacebookDialog + * call are expected. + * + * @param requestCode the request code + * @param resultCode the result code + * @param data the result data + * @param dialogCallback the callback for handling FacebookDialog results, can be null + */ + public void onActivityResult(int requestCode, int resultCode, Intent data, + FacebookDialog.Callback facebookDialogCallback) { + Session session = Session.getActiveSession(); + if (session != null) { + session.onActivityResult(activity, requestCode, resultCode, data); + } + + handleFacebookDialogActivityResult(requestCode, resultCode, data, facebookDialogCallback); + } + + /** + * To be called from an Activity or Fragment's onSaveInstanceState method. + * + * @param outState the bundle to save state in + */ + public void onSaveInstanceState(Bundle outState) { + Session.saveSession(Session.getActiveSession(), outState); + outState.putParcelable(DIALOG_CALL_BUNDLE_SAVE_KEY, pendingFacebookDialogCall); + } + + /** + * To be called from an Activity or Fragment's onPause method. + */ + public void onPause() { + // remove the broadcast receiver + broadcastManager.unregisterReceiver(receiver); + + if (callback != null) { + Session session = Session.getActiveSession(); + if (session != null) { + session.removeCallback(callback); + } + } + } + + /** + * To be called from an Activity or Fragment's onStop method. + */ + public void onStop() { + AppEventsLogger.onContextStop(); + } + + /** + * To be called from an Activity or Fragment's onDestroy method. + */ + public void onDestroy() { + } + + /** + * Register that we are expecting results from a call to the Facebook application (e.g., from a native + * dialog provided by the Facebook app). Activity results forwarded to onActivityResults will be parsed + * and handled if they correspond to this call. Only a single pending FacebookDialog call can be tracked + * at a time; attempting to track another one will cancel the first one. + * @param appCall an PendingCall object containing the call ID + */ + public void trackPendingDialogCall(FacebookDialog.PendingCall pendingCall) { + if (pendingFacebookDialogCall != null) { + // If one is already pending, cancel it; we don't allow multiple pending calls. + Log.i("Facebook", "Tracking new app call while one is still pending; canceling pending call."); + cancelPendingAppCall(null); + } + pendingFacebookDialogCall = pendingCall; + } + + /** + * Retrieves an instance of AppEventsLogger that can be used for the current Session, if any. Different + * instances may be returned if the current Session changes, so this value should not be cached for long + * periods of time -- always call getAppEventsLogger to get the right logger for the current Session. If + * no Session is currently available, this method will return null. + * + * To ensure delivery of app events across Activity lifecycle events, calling Activities should be sure to + * call the onStop method. + * + * @return an AppEventsLogger to use for logging app events + */ + public AppEventsLogger getAppEventsLogger() { + Session session = Session.getActiveSession(); + if (session == null) { + return null; + } + + if (appEventsLogger == null || !appEventsLogger.isValidForSession(session)) { + if (appEventsLogger != null) { + // Pretend we got stopped so the old logger will persist its results now, in case we get stopped + // before events get flushed. + AppEventsLogger.onContextStop(); + } + appEventsLogger = AppEventsLogger.newLogger(activity, session); + } + + return appEventsLogger; + } + + /** + * The BroadcastReceiver implementation that either adds or removes the callback + * from the active Session object as it's SET or UNSET. + */ + private class ActiveSessionBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Session.ACTION_ACTIVE_SESSION_SET.equals(intent.getAction())) { + Session session = Session.getActiveSession(); + if (session != null && callback != null) { + session.addCallback(callback); + } + } else if (Session.ACTION_ACTIVE_SESSION_UNSET.equals(intent.getAction())) { + Session session = Session.getActiveSession(); + if (session != null && callback != null) { + session.removeCallback(callback); + } + } + } + } + + private boolean handleFacebookDialogActivityResult(int requestCode, int resultCode, Intent data, + FacebookDialog.Callback facebookDialogCallback) { + if (pendingFacebookDialogCall == null || pendingFacebookDialogCall.getRequestCode() != requestCode) { + return false; + } + + if (data == null) { + // We understand the request code, but have no Intent. This can happen if the called Activity crashes + // before it can be started; we treat this as a cancellation because we have no other information. + cancelPendingAppCall(facebookDialogCallback); + return true; + } + + String callIdString = data.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID); + UUID callId = null; + if (callIdString != null) { + try { + callId = UUID.fromString(callIdString); + } catch (IllegalArgumentException exception) { + } + } + + // Was this result for the call we are waiting on? + if (callId != null && pendingFacebookDialogCall.getCallId().equals(callId)) { + // Yes, we can handle it normally. + FacebookDialog.handleActivityResult(activity, pendingFacebookDialogCall, requestCode, data, + facebookDialogCallback); + } else { + // No, send a cancellation error to the pending call and ignore the result, because we + // don't know what to do with it. + cancelPendingAppCall(facebookDialogCallback); + } + + pendingFacebookDialogCall = null; + return true; + } + + private void cancelPendingAppCall(FacebookDialog.Callback facebookDialogCallback) { + if (facebookDialogCallback != null) { + Intent pendingIntent = pendingFacebookDialogCall.getRequestIntent(); + + Intent cancelIntent = new Intent(); + cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID, + pendingIntent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID)); + cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION, + pendingIntent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION)); + cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_VERSION, + pendingIntent.getIntExtra(NativeProtocol.EXTRA_PROTOCOL_VERSION, 0)); + cancelIntent.putExtra(NativeProtocol.STATUS_ERROR_TYPE, NativeProtocol.ERROR_UNKNOWN_ERROR); + + FacebookDialog.handleActivityResult(activity, pendingFacebookDialogCall, + pendingFacebookDialogCall.getRequestCode(), cancelIntent, facebookDialogCallback); + } + pendingFacebookDialogCall = null; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/AsyncFacebookRunner.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/AsyncFacebookRunner.java new file mode 100644 index 0000000000..2420fd4b39 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/AsyncFacebookRunner.java @@ -0,0 +1,341 @@ +/** + * Copyright 2010-present Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import android.content.Context; +import android.os.Bundle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; + +/** + * A sample implementation of asynchronous API requests. This class provides + * the ability to execute API methods and have the call return immediately, + * without blocking the calling thread. This is necessary when accessing the + * API in the UI thread, for instance. The request response is returned to + * the caller via a callback interface, which the developer must implement. + * + * This sample implementation simply spawns a new thread for each request, + * and makes the API call immediately. This may work in many applications, + * but more sophisticated users may re-implement this behavior using a thread + * pool, a network thread, a request queue, or other mechanism. Advanced + * functionality could be built, such as rate-limiting of requests, as per + * a specific application's needs. + * + * @deprecated + * + * @see RequestListener + * The callback interface. + * + * @author Jim Brusstar (jimbru@fb.com), + * Yariv Sadan (yariv@fb.com), + * Luke Shepard (lshepard@fb.com) + */ +@Deprecated +public class AsyncFacebookRunner { + + Facebook fb; + + public AsyncFacebookRunner(Facebook fb) { + this.fb = fb; + } + + /** + * Invalidate the current user session by removing the access token in + * memory, clearing the browser cookies, and calling auth.expireSession + * through the API. The application will be notified when logout is + * complete via the callback interface. + *

+ * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + *

+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Session} for more info. + * + * @param context + * The Android context in which the logout should be called: it + * should be the same context in which the login occurred in + * order to clear any stored cookies + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + @Deprecated + public void logout(final Context context, + final RequestListener listener, + final Object state) { + new Thread() { + @Override public void run() { + try { + String response = fb.logoutImpl(context); + if (response.length() == 0 || response.equals("false")){ + listener.onFacebookError(new FacebookError( + "auth.expireSession failed"), state); + return; + } + listener.onComplete(response, state); + } catch (FileNotFoundException e) { + listener.onFileNotFoundException(e, state); + } catch (MalformedURLException e) { + listener.onMalformedURLException(e, state); + } catch (IOException e) { + listener.onIOException(e, state); + } + } + }.start(); + } + + @Deprecated + public void logout(final Context context, final RequestListener listener) { + logout(context, listener, /* state */ null); + } + + /** + * Make a request to Facebook's old (pre-graph) API with the given + * parameters. One of the parameter keys must be "method" and its value + * should be a valid REST server API method. + *

+ * See http://developers.facebook.com/docs/reference/rest/ + *

+ * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + *

+ * Example: + * + * Bundle parameters = new Bundle(); + * parameters.putString("method", "auth.expireSession", new Listener()); + * String response = request(parameters); + * + *

+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info. + * + * @param parameters + * Key-value pairs of parameters to the request. Refer to the + * documentation: one of the parameters must be "method". + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + @Deprecated + public void request(Bundle parameters, + RequestListener listener, + final Object state) { + request(null, parameters, "GET", listener, state); + } + + @Deprecated + public void request(Bundle parameters, RequestListener listener) { + request(null, parameters, "GET", listener, /* state */ null); + } + + /** + * Make a request to the Facebook Graph API without any parameters. + *

+ * See http://developers.facebook.com/docs/api + *

+ * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + *

+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + @Deprecated + public void request(String graphPath, + RequestListener listener, + final Object state) { + request(graphPath, new Bundle(), "GET", listener, state); + } + + @Deprecated + public void request(String graphPath, RequestListener listener) { + request(graphPath, new Bundle(), "GET", listener, /* state */ null); + } + + /** + * Make a request to the Facebook Graph API with the given string parameters + * using an HTTP GET (default method). + *

+ * See http://developers.facebook.com/docs/api + *

+ * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + *

+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters "q" : "facebook" would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + @Deprecated + public void request(String graphPath, + Bundle parameters, + RequestListener listener, + final Object state) { + request(graphPath, parameters, "GET", listener, state); + } + + @Deprecated + public void request(String graphPath, + Bundle parameters, + RequestListener listener) { + request(graphPath, parameters, "GET", listener, /* state */ null); + } + + /** + * Make a request to the Facebook Graph API with the given HTTP method and + * string parameters. Note that binary data parameters (e.g. pictures) are + * not yet supported by this helper function. + *

+ * See http://developers.facebook.com/docs/api + *

+ * Note that this method is asynchronous and the callback will be invoked + * in a background thread; operations that affect the UI will need to be + * posted to the UI thread or an appropriate handler. + *

+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters {"q" : "facebook"} would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param httpMethod + * http verb, e.g. "POST", "DELETE" + * @param listener + * Callback interface to notify the application when the request + * has completed. + * @param state + * An arbitrary object used to identify the request when it + * returns to the callback. This has no effect on the request + * itself. + */ + @Deprecated + public void request(final String graphPath, + final Bundle parameters, + final String httpMethod, + final RequestListener listener, + final Object state) { + new Thread() { + @Override public void run() { + try { + String resp = fb.requestImpl(graphPath, parameters, httpMethod); + listener.onComplete(resp, state); + } catch (FileNotFoundException e) { + listener.onFileNotFoundException(e, state); + } catch (MalformedURLException e) { + listener.onMalformedURLException(e, state); + } catch (IOException e) { + listener.onIOException(e, state); + } + } + }.start(); + } + + /** + * Callback interface for API requests. + *

+ * Each method includes a 'state' parameter that identifies the calling + * request. It will be set to the value passed when originally calling the + * request method, or null if none was passed. + *

+ * This interface is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info. + */ + @Deprecated + public static interface RequestListener { + + /** + * Called when a request completes with the given response. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onComplete(String response, Object state); + + /** + * Called when a request has a network or request error. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onIOException(IOException e, Object state); + + /** + * Called when a request fails because the requested resource is + * invalid or does not exist. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onFileNotFoundException(FileNotFoundException e, + Object state); + + /** + * Called if an invalid graph path is provided (which may result in a + * malformed URL). + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onMalformedURLException(MalformedURLException e, + Object state); + + /** + * Called when the server-side Facebook method fails. + * + * Executed by a background thread: do not update the UI in this method. + */ + public void onFacebookError(FacebookError e, Object state); + + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/DialogError.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/DialogError.java new file mode 100644 index 0000000000..a99c4e6132 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/DialogError.java @@ -0,0 +1,64 @@ +/** + * Copyright 2010-present Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +/** + * Encapsulation of Dialog Error. + *

+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED. + *

+ * All public members of this class are intentionally deprecated. + * New code should instead use + * {@link com.facebook.FacebookException} + *

+ * Adding @Deprecated to this class causes warnings in other deprecated classes + * that reference this one. That is the only reason this entire class is not + * deprecated. + * + * @devDocDeprecated + */ +public class DialogError extends Throwable { + + private static final long serialVersionUID = 1L; + + /** + * The ErrorCode received by the WebView: see + * http://developer.android.com/reference/android/webkit/WebViewClient.html + */ + private int mErrorCode; + + /** The URL that the dialog was trying to load */ + private String mFailingUrl; + + @Deprecated + public DialogError(String message, int errorCode, String failingUrl) { + super(message); + mErrorCode = errorCode; + mFailingUrl = failingUrl; + } + + @Deprecated + public int getErrorCode() { + return mErrorCode; + } + + @Deprecated + public String getFailingUrl() { + return mFailingUrl; + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/Facebook.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/Facebook.java new file mode 100644 index 0000000000..83cdabe9bc --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/Facebook.java @@ -0,0 +1,1356 @@ +/** + * Copyright 2010-present Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import android.Manifest; +import android.app.Activity; +import android.content.*; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.Signature; +import android.net.Uri; +import android.os.*; +import com.facebook.*; +import com.facebook.Session.StatusCallback; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.net.MalformedURLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * THIS CLASS SHOULD BE CONSIDERED DEPRECATED. + *

+ * All public members of this class are intentionally deprecated. + * New code should instead use + * {@link Session} to manage session state, + * {@link Request} to make API requests, and + * {@link com.facebook.widget.WebDialog} to make dialog requests. + *

+ * Adding @Deprecated to this class causes warnings in other deprecated classes + * that reference this one. That is the only reason this entire class is not + * deprecated. + * + * @devDocDeprecated + */ +public class Facebook { + + // Strings used in the authorization flow + @Deprecated + public static final String REDIRECT_URI = "fbconnect://success"; + @Deprecated + public static final String CANCEL_URI = "fbconnect://cancel"; + @Deprecated + public static final String TOKEN = "access_token"; + @Deprecated + public static final String EXPIRES = "expires_in"; + @Deprecated + public static final String SINGLE_SIGN_ON_DISABLED = "service_disabled"; + + @Deprecated + public static final Uri ATTRIBUTION_ID_CONTENT_URI = + Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider"); + @Deprecated + public static final String ATTRIBUTION_ID_COLUMN_NAME = "aid"; + + @Deprecated + public static final int FORCE_DIALOG_AUTH = -1; + + private static final String LOGIN = "oauth"; + + // Used as default activityCode by authorize(). See authorize() below. + private static final int DEFAULT_AUTH_ACTIVITY_CODE = 32665; + + // Facebook server endpoints: may be modified in a subclass for testing + @Deprecated + protected static String DIALOG_BASE_URL = "https://m.facebook.com/dialog/"; + @Deprecated + protected static String GRAPH_BASE_URL = "https://graph.facebook.com/"; + @Deprecated + protected static String RESTSERVER_URL = "https://api.facebook.com/restserver.php"; + + private final Object lock = new Object(); + + private String accessToken = null; + private long accessExpiresMillisecondsAfterEpoch = 0; + private long lastAccessUpdateMillisecondsAfterEpoch = 0; + private String mAppId; + + private Activity pendingAuthorizationActivity; + private String[] pendingAuthorizationPermissions; + private Session pendingOpeningSession; + + private volatile Session session; // must synchronize this.sync to write + private boolean sessionInvalidated; // must synchronize this.sync to access + private SetterTokenCachingStrategy tokenCache; + private volatile Session userSetSession; + + // If the last time we extended the access token was more than 24 hours ago + // we try to refresh the access token again. + final private static long REFRESH_TOKEN_BARRIER = 24L * 60L * 60L * 1000L; + + /** + * Constructor for Facebook object. + * + * @param appId + * Your Facebook application ID. Found at + * www.facebook.com/developers/apps.php. + */ + @Deprecated + public Facebook(String appId) { + if (appId == null) { + throw new IllegalArgumentException("You must specify your application ID when instantiating " + + "a Facebook object. See README for details."); + } + mAppId = appId; + } + + /** + * Default authorize method. Grants only basic permissions. + *

+ * See authorize() below for @params. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + */ + @Deprecated + public void authorize(Activity activity, final DialogListener listener) { + authorize(activity, new String[]{}, DEFAULT_AUTH_ACTIVITY_CODE, SessionLoginBehavior.SSO_WITH_FALLBACK, + listener); + } + + /** + * Authorize method that grants custom permissions. + *

+ * See authorize() below for @params. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + */ + @Deprecated + public void authorize(Activity activity, String[] permissions, final DialogListener listener) { + authorize(activity, permissions, DEFAULT_AUTH_ACTIVITY_CODE, SessionLoginBehavior.SSO_WITH_FALLBACK, listener); + } + + /** + * Full authorize method. + *

+ * Starts either an Activity or a dialog which prompts the user to log in to + * Facebook and grant the requested permissions to the given application. + *

+ * This method will, when possible, use Facebook's single sign-on for + * Android to obtain an access token. This involves proxying a call through + * the Facebook for Android stand-alone application, which will handle the + * authentication flow, and return an OAuth access token for making API + * calls. + *

+ * Because this process will not be available for all users, if single + * sign-on is not possible, this method will automatically fall back to the + * OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled + * by Facebook in an embedded WebView, not by the client application. As + * such, the dialog makes a network request and renders HTML content rather + * than a native UI. The access token is retrieved from a redirect to a + * special URL that the WebView handles. + *

+ * Note that User credentials could be handled natively using the OAuth 2.0 + * Username and Password Flow, but this is not supported by this SDK. + *

+ * See http://developers.facebook.com/docs/authentication/ and + * http://wiki.oauth.net/OAuth-2 for more details. + *

+ * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + *

+ * Also note that requests may be made to the API without calling authorize + * first, in which case only public information is returned. + *

+ * IMPORTANT: Note that single sign-on authentication will not function + * correctly if you do not include a call to the authorizeCallback() method + * in your onActivityResult() function! Please see below for more + * information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH + * as the activityCode parameter in your call to authorize(). + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param activity + * The Android activity in which we want to display the + * authorization dialog. + * @param permissions + * A list of permissions required for this application: e.g. + * "read_stream", "publish_stream", "offline_access", etc. see + * http://developers.facebook.com/docs/authentication/permissions + * This parameter should not be null -- if you do not require any + * permissions, then pass in an empty String array. + * @param activityCode + * Single sign-on requires an activity result to be called back + * to the client application -- if you are waiting on other + * activities to return data, pass a custom activity code here to + * avoid collisions. If you would like to force the use of legacy + * dialog-based authorization, pass FORCE_DIALOG_AUTH for this + * parameter. Otherwise just omit this parameter and Facebook + * will use a suitable default. See + * http://developer.android.com/reference/android/ + * app/Activity.html for more information. + * @param listener + * Callback interface for notifying the calling application when + * the authentication dialog has completed, failed, or been + * canceled. + */ + @Deprecated + public void authorize(Activity activity, String[] permissions, int activityCode, final DialogListener listener) { + SessionLoginBehavior behavior = (activityCode >= 0) ? SessionLoginBehavior.SSO_WITH_FALLBACK + : SessionLoginBehavior.SUPPRESS_SSO; + + authorize(activity, permissions, activityCode, behavior, listener); + } + + /** + * Full authorize method. + * + * Starts either an Activity or a dialog which prompts the user to log in to + * Facebook and grant the requested permissions to the given application. + * + * This method will, when possible, use Facebook's single sign-on for + * Android to obtain an access token. This involves proxying a call through + * the Facebook for Android stand-alone application, which will handle the + * authentication flow, and return an OAuth access token for making API + * calls. + * + * Because this process will not be available for all users, if single + * sign-on is not possible, this method will automatically fall back to the + * OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled + * by Facebook in an embedded WebView, not by the client application. As + * such, the dialog makes a network request and renders HTML content rather + * than a native UI. The access token is retrieved from a redirect to a + * special URL that the WebView handles. + * + * Note that User credentials could be handled natively using the OAuth 2.0 + * Username and Password Flow, but this is not supported by this SDK. + * + * See http://developers.facebook.com/docs/authentication/ and + * http://wiki.oauth.net/OAuth-2 for more details. + * + * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + * + * Also note that requests may be made to the API without calling authorize + * first, in which case only public information is returned. + * + * IMPORTANT: Note that single sign-on authentication will not function + * correctly if you do not include a call to the authorizeCallback() method + * in your onActivityResult() function! Please see below for more + * information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH + * as the activityCode parameter in your call to authorize(). + * + * @param activity + * The Android activity in which we want to display the + * authorization dialog. + * @param permissions + * A list of permissions required for this application: e.g. + * "read_stream", "publish_stream", "offline_access", etc. see + * http://developers.facebook.com/docs/authentication/permissions + * This parameter should not be null -- if you do not require any + * permissions, then pass in an empty String array. + * @param activityCode + * Single sign-on requires an activity result to be called back + * to the client application -- if you are waiting on other + * activities to return data, pass a custom activity code here to + * avoid collisions. If you would like to force the use of legacy + * dialog-based authorization, pass FORCE_DIALOG_AUTH for this + * parameter. Otherwise just omit this parameter and Facebook + * will use a suitable default. See + * http://developer.android.com/reference/android/ + * app/Activity.html for more information. + * @param behavior + * The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + * @param listener + * Callback interface for notifying the calling application when + * the authentication dialog has completed, failed, or been + * canceled. + */ + private void authorize(Activity activity, String[] permissions, int activityCode, + SessionLoginBehavior behavior, final DialogListener listener) { + checkUserSession("authorize"); + pendingOpeningSession = new Session.Builder(activity). + setApplicationId(mAppId). + setTokenCachingStrategy(getTokenCache()). + build(); + pendingAuthorizationActivity = activity; + pendingAuthorizationPermissions = (permissions != null) ? permissions : new String[0]; + + StatusCallback callback = new StatusCallback() { + @Override + public void call(Session callbackSession, SessionState state, Exception exception) { + // Invoke user-callback. + onSessionCallback(callbackSession, state, exception, listener); + } + }; + + Session.OpenRequest openRequest = new Session.OpenRequest(activity). + setCallback(callback). + setLoginBehavior(behavior). + setRequestCode(activityCode). + setPermissions(Arrays.asList(pendingAuthorizationPermissions)); + openSession(pendingOpeningSession, openRequest, pendingAuthorizationPermissions.length > 0); + } + + private void openSession(Session session, Session.OpenRequest openRequest, boolean isPublish) { + openRequest.setIsLegacy(true); + if (isPublish) { + session.openForPublish(openRequest); + } else { + session.openForRead(openRequest); + } + } + + @SuppressWarnings("deprecation") + private void onSessionCallback(Session callbackSession, SessionState state, Exception exception, + DialogListener listener) { + Bundle extras = callbackSession.getAuthorizationBundle(); + + if (state == SessionState.OPENED) { + Session sessionToClose = null; + + synchronized (Facebook.this.lock) { + if (callbackSession != Facebook.this.session) { + sessionToClose = Facebook.this.session; + Facebook.this.session = callbackSession; + Facebook.this.sessionInvalidated = false; + } + } + + if (sessionToClose != null) { + sessionToClose.close(); + } + + listener.onComplete(extras); + } else if (exception != null) { + if (exception instanceof FacebookOperationCanceledException) { + listener.onCancel(); + } else if ((exception instanceof FacebookAuthorizationException) && (extras != null) + && extras.containsKey(Session.WEB_VIEW_ERROR_CODE_KEY) + && extras.containsKey(Session.WEB_VIEW_FAILING_URL_KEY)) { + DialogError error = new DialogError(exception.getMessage(), + extras.getInt(Session.WEB_VIEW_ERROR_CODE_KEY), + extras.getString(Session.WEB_VIEW_FAILING_URL_KEY)); + listener.onError(error); + } else { + FacebookError error = new FacebookError(exception.getMessage()); + listener.onFacebookError(error); + } + } + } + + /** + * Helper to validate a service intent by resolving and checking the + * provider's package signature. + * + * @param context + * @param intent + * @return true if the service intent resolution happens successfully and + * the signatures match. + */ + private boolean validateServiceIntent(Context context, Intent intent) { + ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent, 0); + if (resolveInfo == null) { + return false; + } + + return validateAppSignatureForPackage(context, resolveInfo.serviceInfo.packageName); + } + + /** + * Query the signature for the application that would be invoked by the + * given intent and verify that it matches the FB application's signature. + * + * @param context + * @param packageName + * @return true if the app's signature matches the expected signature. + */ + private boolean validateAppSignatureForPackage(Context context, String packageName) { + + PackageInfo packageInfo; + try { + packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + return false; + } + + for (Signature signature : packageInfo.signatures) { + if (signature.toCharsString().equals(FB_APP_SIGNATURE)) { + return true; + } + } + return false; + } + + /** + * IMPORTANT: If you are using the deprecated authorize() method, + * this method must be invoked at the top of the calling + * activity's onActivityResult() function or Facebook authentication will + * not function properly! + *

+ * If your calling activity does not currently implement onActivityResult(), + * you must implement it and include a call to this method if you intend to + * use the authorize() method in this SDK. + *

+ * For more information, see + * http://developer.android.com/reference/android/app/ + * Activity.html#onActivityResult(int, int, android.content.Intent) + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + */ + @Deprecated + public void authorizeCallback(int requestCode, int resultCode, Intent data) { + checkUserSession("authorizeCallback"); + Session pending = this.pendingOpeningSession; + if (pending != null) { + if (pending.onActivityResult(this.pendingAuthorizationActivity, requestCode, resultCode, data)) { + this.pendingOpeningSession = null; + this.pendingAuthorizationActivity = null; + this.pendingAuthorizationPermissions = null; + } + } + } + + /** + * Refresh OAuth access token method. Binds to Facebook for Android + * stand-alone application application to refresh the access token. This + * method tries to connect to the Facebook App which will handle the + * authentication flow, and return a new OAuth access token. This method + * will automatically replace the old token with a new one. Note that this + * method is asynchronous and the callback will be invoked in the original + * calling thread (not in a background thread). + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param context + * The Android Context that will be used to bind to the Facebook + * RefreshToken Service + * @param serviceListener + * Callback interface for notifying the calling application when + * the refresh request has completed or failed (can be null). In + * case of a success a new token can be found inside the result + * Bundle under Facebook.ACCESS_TOKEN key. + * @return true if the binding to the RefreshToken Service was created + */ + @Deprecated + public boolean extendAccessToken(Context context, ServiceListener serviceListener) { + checkUserSession("extendAccessToken"); + Intent intent = new Intent(); + + intent.setClassName("com.facebook.katana", "com.facebook.katana.platform.TokenRefreshService"); + + // Verify that the application whose package name is + // com.facebook.katana + // has the expected FB app signature. + if (!validateServiceIntent(context, intent)) { + return false; + } + + return context.bindService(intent, new TokenRefreshServiceConnection(context, serviceListener), + Context.BIND_AUTO_CREATE); + } + + /** + * Calls extendAccessToken if shouldExtendAccessToken returns true. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @return the same value as extendAccessToken if the the token requires + * refreshing, true otherwise + */ + @Deprecated + public boolean extendAccessTokenIfNeeded(Context context, ServiceListener serviceListener) { + checkUserSession("extendAccessTokenIfNeeded"); + if (shouldExtendAccessToken()) { + return extendAccessToken(context, serviceListener); + } + return true; + } + + /** + * Check if the access token requires refreshing. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @return true if the last time a new token was obtained was over 24 hours + * ago. + */ + @Deprecated + public boolean shouldExtendAccessToken() { + checkUserSession("shouldExtendAccessToken"); + return isSessionValid() + && (System.currentTimeMillis() - lastAccessUpdateMillisecondsAfterEpoch >= REFRESH_TOKEN_BARRIER); + } + + /** + * Handles connection to the token refresh service (this service is a part + * of Facebook App). + */ + private class TokenRefreshServiceConnection implements ServiceConnection { + + final Messenger messageReceiver = new Messenger( + new TokenRefreshConnectionHandler(Facebook.this, this)); + + final ServiceListener serviceListener; + final Context applicationsContext; + + Messenger messageSender = null; + + public TokenRefreshServiceConnection(Context applicationsContext, ServiceListener serviceListener) { + this.applicationsContext = applicationsContext; + this.serviceListener = serviceListener; + } + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + messageSender = new Messenger(service); + refreshToken(); + } + + @Override + public void onServiceDisconnected(ComponentName arg) { + serviceListener.onError(new Error("Service disconnected")); + // We returned an error so there's no point in + // keeping the binding open. + applicationsContext.unbindService(TokenRefreshServiceConnection.this); + } + + private void refreshToken() { + Bundle requestData = new Bundle(); + requestData.putString(TOKEN, accessToken); + + Message request = Message.obtain(); + request.setData(requestData); + request.replyTo = messageReceiver; + + try { + messageSender.send(request); + } catch (RemoteException e) { + serviceListener.onError(new Error("Service connection error")); + } + } + } + + // Creating a static Handler class to reduce the possibility of a memory leak. + // Handler objects for the same thread all share a common Looper object, which they post messages + // to and read from. As messages contain target Handler, as long as there are messages with target + // handler in the message queue, the handler cannot be garbage collected. If handler is not static, + // the instance of the containing class also cannot be garbage collected even if it is destroyed. + private static class TokenRefreshConnectionHandler extends Handler { + WeakReference facebookWeakReference; + WeakReference connectionWeakReference; + + TokenRefreshConnectionHandler(Facebook facebook, TokenRefreshServiceConnection connection) { + super(); + facebookWeakReference = new WeakReference(facebook); + connectionWeakReference = new WeakReference(connection); + } + + @Override + @SuppressWarnings("deprecation") + public void handleMessage(Message msg) { + Facebook facebook = facebookWeakReference.get(); + TokenRefreshServiceConnection connection = connectionWeakReference.get(); + if (facebook == null || connection == null) { + return; + } + + String token = msg.getData().getString(TOKEN); + // Legacy functions in Facebook class (and ServiceListener implementors) expect expires_in in + // milliseconds from epoch + long expiresAtMsecFromEpoch = msg.getData().getLong(EXPIRES) * 1000L; + + if (token != null) { + facebook.setAccessToken(token); + facebook.setAccessExpires(expiresAtMsecFromEpoch); + + Session refreshSession = facebook.session; + if (refreshSession != null) { + // Session.internalRefreshToken expects the original bundle with expires_in in seconds from + // epoch. + LegacyHelper.extendTokenCompleted(refreshSession, msg.getData()); + } + + if (connection.serviceListener != null) { + // To avoid confusion we should return the expiration time in + // the same format as the getAccessExpires() function - that + // is in milliseconds. + Bundle resultBundle = (Bundle) msg.getData().clone(); + resultBundle.putLong(EXPIRES, expiresAtMsecFromEpoch); + + connection.serviceListener.onComplete(resultBundle); + } + } else if (connection.serviceListener != null) { // extract errors only if + // client wants them + String error = msg.getData().getString("error"); + if (msg.getData().containsKey("error_code")) { + int errorCode = msg.getData().getInt("error_code"); + connection.serviceListener.onFacebookError(new FacebookError(error, null, errorCode)); + } else { + connection.serviceListener.onError(new Error(error != null ? error : "Unknown service error")); + } + } + + // The refreshToken function should be called rarely, + // so there is no point in keeping the binding open. + connection.applicationsContext.unbindService(connection); + } + } + + /** + * Invalidate the current user session by removing the access token in + * memory, clearing the browser cookie, and calling auth.expireSession + * through the API. + *

+ * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param context + * The Android context in which the logout should be called: it + * should be the same context in which the login occurred in + * order to clear any stored cookies + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the auth.expireSession response + * ("true" if successful) + */ + @Deprecated + public String logout(Context context) throws MalformedURLException, IOException { + return logoutImpl(context); + } + + String logoutImpl(Context context) throws MalformedURLException, IOException { + checkUserSession("logout"); + Bundle b = new Bundle(); + b.putString("method", "auth.expireSession"); + String response = request(b); + + long currentTimeMillis = System.currentTimeMillis(); + Session sessionToClose = null; + + synchronized (this.lock) { + sessionToClose = session; + + session = null; + accessToken = null; + accessExpiresMillisecondsAfterEpoch = 0; + lastAccessUpdateMillisecondsAfterEpoch = currentTimeMillis; + sessionInvalidated = false; + } + + if (sessionToClose != null) { + sessionToClose.closeAndClearTokenInformation(); + } + + return response; + } + + /** + * Make a request to Facebook's old (pre-graph) API with the given + * parameters. One of the parameter keys must be "method" and its value + * should be a valid REST server API method. + *

+ * See http://developers.facebook.com/docs/reference/rest/ + *

+ * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + *

+ * Example: + * Bundle parameters = new Bundle(); + * parameters.putString("method", "auth.expireSession"); + * String response = request(parameters); + * + *

+ * This method is deprecated. See {@link Facebook} and {@link Request} for more info. + * + * @param parameters + * Key-value pairs of parameters to the request. Refer to the + * documentation: one of the parameters must be "method". + * @throws IOException + * if a network error occurs + * @throws MalformedURLException + * if accessing an invalid endpoint + * @throws IllegalArgumentException + * if one of the parameters is not "method" + * @return JSON string representation of the response + */ + @Deprecated + public String request(Bundle parameters) throws MalformedURLException, IOException { + if (!parameters.containsKey("method")) { + throw new IllegalArgumentException("API method must be specified. " + + "(parameters must contain key \"method\" and value). See" + + " http://developers.facebook.com/docs/reference/rest/"); + } + return requestImpl(null, parameters, "GET"); + } + + /** + * Make a request to the Facebook Graph API without any parameters. + *

+ * See http://developers.facebook.com/docs/api + *

+ * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + *

+ * This method is deprecated. See {@link Facebook} and {@link Request} for more info. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + @Deprecated + public String request(String graphPath) throws MalformedURLException, IOException { + return requestImpl(graphPath, new Bundle(), "GET"); + } + + /** + * Make a request to the Facebook Graph API with the given string parameters + * using an HTTP GET (default method). + *

+ * See http://developers.facebook.com/docs/api + *

+ * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + *

+ * This method is deprecated. See {@link Facebook} and {@link Request} for more info. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param parameters + * key-value string parameters, e.g. the path "search" with + * parameters "q" : "facebook" would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + @Deprecated + public String request(String graphPath, Bundle parameters) throws MalformedURLException, IOException { + return requestImpl(graphPath, parameters, "GET"); + } + + /** + * Synchronously make a request to the Facebook Graph API with the given + * HTTP method and string parameters. Note that binary data parameters (e.g. + * pictures) are not yet supported by this helper function. + *

+ * See http://developers.facebook.com/docs/api + *

+ * Note that this method blocks waiting for a network response, so do not + * call it in a UI thread. + *

+ * This method is deprecated. See {@link Facebook} and {@link Request} for more info. + * + * @param graphPath + * Path to resource in the Facebook graph, e.g., to fetch data + * about the currently logged authenticated user, provide "me", + * which will fetch http://graph.facebook.com/me + * @param params + * Key-value string parameters, e.g. the path "search" with + * parameters {"q" : "facebook"} would produce a query for the + * following graph resource: + * https://graph.facebook.com/search?q=facebook + * @param httpMethod + * http verb, e.g. "GET", "POST", "DELETE" + * @throws IOException + * @throws MalformedURLException + * @return JSON string representation of the response + */ + @Deprecated + public String request(String graphPath, Bundle params, String httpMethod) throws FileNotFoundException, + MalformedURLException, IOException { + return requestImpl(graphPath, params, httpMethod); + } + + // Internal call to avoid deprecated warnings. + @SuppressWarnings("deprecation") + String requestImpl(String graphPath, Bundle params, String httpMethod) throws FileNotFoundException, + MalformedURLException, IOException { + params.putString("format", "json"); + if (isSessionValid()) { + params.putString(TOKEN, getAccessToken()); + } + String url = (graphPath != null) ? GRAPH_BASE_URL + graphPath : RESTSERVER_URL; + return Util.openUrl(url, httpMethod, params); + } + + /** + * Generate a UI dialog for the request action in the given Android context. + *

+ * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + * + * This method is deprecated. See {@link com.facebook.widget.WebDialog}. + * + * @param context + * The Android context in which we will generate this dialog. + * @param action + * String representation of the desired method: e.g. "login", + * "stream.publish", ... + * @param listener + * Callback interface to notify the application when the dialog + * has completed. + */ + @Deprecated + public void dialog(Context context, String action, DialogListener listener) { + dialog(context, action, new Bundle(), listener); + } + + /** + * Generate a UI dialog for the request action in the given Android context + * with the provided parameters. + *

+ * Note that this method is asynchronous and the callback will be invoked in + * the original calling thread (not in a background thread). + * + * This method is deprecated. See {@link com.facebook.widget.WebDialog}. + * + * @param context + * The Android context in which we will generate this dialog. + * @param action + * String representation of the desired method: e.g. "feed" ... + * @param parameters + * String key-value pairs to be passed as URL parameters. + * @param listener + * Callback interface to notify the application when the dialog + * has completed. + */ + @Deprecated + public void dialog(Context context, String action, Bundle parameters, final DialogListener listener) { + parameters.putString("display", "touch"); + parameters.putString("redirect_uri", REDIRECT_URI); + + if (action.equals(LOGIN)) { + parameters.putString("type", "user_agent"); + parameters.putString("client_id", mAppId); + } else { + parameters.putString("app_id", mAppId); + // We do not want to add an access token when displaying the auth dialog. + if (isSessionValid()) { + parameters.putString(TOKEN, getAccessToken()); + } + } + + if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { + Util.showAlert(context, "Error", "Application requires permission to access the Internet"); + } else { + new FbDialog(context, action, parameters, listener).show(); + } + } + + /** + * Returns whether the current access token is valid + * + * @return boolean - whether this object has an non-expired session token + */ + @Deprecated + public boolean isSessionValid() { + return (getAccessToken() != null) + && ((getAccessExpires() == 0) || (System.currentTimeMillis() < getAccessExpires())); + } + + /** + * Allows the user to set a Session for the Facebook class to use. + * If a Session is set here, then one should not use the authorize, logout, + * or extendAccessToken methods which alter the Session object since that may + * result in undefined behavior. Using those methods after setting the + * session here will result in exceptions being thrown. + * + * @param session the Session object to use, cannot be null + */ + @Deprecated + public void setSession(Session session) { + if (session == null) { + throw new IllegalArgumentException("session cannot be null"); + } + synchronized (this.lock) { + this.userSetSession = session; + } + } + + private void checkUserSession(String methodName) { + if (userSetSession != null) { + throw new UnsupportedOperationException( + String.format("Cannot call %s after setSession has been called.", methodName)); + } + } + + /** + * Get the underlying Session object to use with 3.0 api. + * + * @return Session - underlying session + */ + @Deprecated + public final Session getSession() { + while (true) { + String cachedToken = null; + Session oldSession = null; + + synchronized (this.lock) { + if (userSetSession != null) { + return userSetSession; + } + if ((session != null) || !sessionInvalidated) { + return session; + } + + cachedToken = accessToken; + oldSession = session; + } + + if (cachedToken == null) { + return null; + } + + // At this point we do not have a valid session, but mAccessToken is + // non-null. + // So we can try building a session based on that. + List permissions; + if (oldSession != null) { + permissions = oldSession.getPermissions(); + } else if (pendingAuthorizationPermissions != null) { + permissions = Arrays.asList(pendingAuthorizationPermissions); + } else { + permissions = Collections.emptyList(); + } + + Session newSession = new Session.Builder(pendingAuthorizationActivity). + setApplicationId(mAppId). + setTokenCachingStrategy(getTokenCache()). + build(); + if (newSession.getState() != SessionState.CREATED_TOKEN_LOADED) { + return null; + } + Session.OpenRequest openRequest = + new Session.OpenRequest(pendingAuthorizationActivity).setPermissions(permissions); + openSession(newSession, openRequest, !permissions.isEmpty()); + + Session invalidatedSession = null; + Session returnSession = null; + + synchronized (this.lock) { + if (sessionInvalidated || (session == null)) { + invalidatedSession = session; + returnSession = session = newSession; + sessionInvalidated = false; + } + } + + if (invalidatedSession != null) { + invalidatedSession.close(); + } + + if (returnSession != null) { + return returnSession; + } + // Else token state changed between the synchronized blocks, so + // retry.. + } + } + + /** + * Retrieve the OAuth 2.0 access token for API access: treat with care. + * Returns null if no session exists. + * + * @return String - access token + */ + @Deprecated + public String getAccessToken() { + Session s = getSession(); + if (s != null) { + return s.getAccessToken(); + } else { + return null; + } + } + + /** + * Retrieve the current session's expiration time (in milliseconds since + * Unix epoch), or 0 if the session doesn't expire or doesn't exist. + * + * @return long - session expiration time + */ + @Deprecated + public long getAccessExpires() { + Session s = getSession(); + if (s != null) { + return s.getExpirationDate().getTime(); + } else { + return accessExpiresMillisecondsAfterEpoch; + } + } + + /** + * Retrieve the last time the token was updated (in milliseconds since + * the Unix epoch), or 0 if the token has not been set. + * + * @return long - timestamp of the last token update. + */ + @Deprecated + public long getLastAccessUpdate() { + return lastAccessUpdateMillisecondsAfterEpoch; + } + + /** + * Restore the token, expiration time, and last update time from cached values. + * These should be values obtained from getAccessToken(), getAccessExpires, and + * getLastAccessUpdate() respectively. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param accessToken - access token + * @param accessExpires - access token expiration time + * @param lastAccessUpdate - timestamp of the last token update + */ + @Deprecated + public void setTokenFromCache(String accessToken, long accessExpires, long lastAccessUpdate) { + checkUserSession("setTokenFromCache"); + synchronized (this.lock) { + this.accessToken = accessToken; + accessExpiresMillisecondsAfterEpoch = accessExpires; + lastAccessUpdateMillisecondsAfterEpoch = lastAccessUpdate; + } + } + + /** + * Set the OAuth 2.0 access token for API access. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param token + * - access token + */ + @Deprecated + public void setAccessToken(String token) { + checkUserSession("setAccessToken"); + synchronized (this.lock) { + accessToken = token; + lastAccessUpdateMillisecondsAfterEpoch = System.currentTimeMillis(); + sessionInvalidated = true; + } + } + + /** + * Set the current session's expiration time (in milliseconds since Unix + * epoch), or 0 if the session doesn't expire. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param timestampInMsec + * - timestamp in milliseconds + */ + @Deprecated + public void setAccessExpires(long timestampInMsec) { + checkUserSession("setAccessExpires"); + synchronized (this.lock) { + accessExpiresMillisecondsAfterEpoch = timestampInMsec; + lastAccessUpdateMillisecondsAfterEpoch = System.currentTimeMillis(); + sessionInvalidated = true; + } + } + + /** + * Set the current session's duration (in seconds since Unix epoch), or "0" + * if session doesn't expire. + *

+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param expiresInSecsFromNow + * - duration in seconds (or 0 if the session doesn't expire) + */ + @Deprecated + public void setAccessExpiresIn(String expiresInSecsFromNow) { + checkUserSession("setAccessExpiresIn"); + if (expiresInSecsFromNow != null) { + long expires = expiresInSecsFromNow.equals("0") ? 0 : System.currentTimeMillis() + + Long.parseLong(expiresInSecsFromNow) * 1000L; + setAccessExpires(expires); + } + } + + /** + * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @return the String representing application ID + */ + @Deprecated + public String getAppId() { + return mAppId; + } + + /** + * This method is deprecated. See {@link Facebook} and {@link Session} for more info. + * + * @param appId the String representing the application ID + */ + @Deprecated + public void setAppId(String appId) { + checkUserSession("setAppId"); + synchronized (this.lock) { + mAppId = appId; + sessionInvalidated = true; + } + } + + private TokenCachingStrategy getTokenCache() { + // Intentionally not volatile/synchronized--it is okay if we race to + // create more than one of these. + if (tokenCache == null) { + tokenCache = new SetterTokenCachingStrategy(); + } + return tokenCache; + } + + private static String[] stringArray(List list) { + int size = (list != null) ? list.size() : 0; + String[] array = new String[size]; + + if (list != null) { + for (int i = 0; i < array.length; i++) { + array[i] = list.get(i); + } + } + + return array; + } + + private static List stringList(String[] array) { + if (array != null) { + return Arrays.asList(array); + } else { + return Collections.emptyList(); + } + } + + private class SetterTokenCachingStrategy extends TokenCachingStrategy { + + @Override + public Bundle load() { + Bundle bundle = new Bundle(); + + if (accessToken != null) { + TokenCachingStrategy.putToken(bundle, accessToken); + TokenCachingStrategy.putExpirationMilliseconds(bundle, accessExpiresMillisecondsAfterEpoch); + TokenCachingStrategy.putPermissions(bundle, stringList(pendingAuthorizationPermissions)); + TokenCachingStrategy.putSource(bundle, AccessTokenSource.WEB_VIEW); + TokenCachingStrategy.putLastRefreshMilliseconds(bundle, lastAccessUpdateMillisecondsAfterEpoch); + } + + return bundle; + } + + @Override + public void save(Bundle bundle) { + accessToken = TokenCachingStrategy.getToken(bundle); + accessExpiresMillisecondsAfterEpoch = TokenCachingStrategy.getExpirationMilliseconds(bundle); + pendingAuthorizationPermissions = stringArray(TokenCachingStrategy.getPermissions(bundle)); + lastAccessUpdateMillisecondsAfterEpoch = TokenCachingStrategy.getLastRefreshMilliseconds(bundle); + } + + @Override + public void clear() { + accessToken = null; + } + } + + /** + * Get Attribution ID for app install conversion tracking. + *

+ * This method is deprecated. See {@link Facebook} and {@link Settings} for more info. + * + * @param contentResolver + * @return Attribution ID that will be used for conversion tracking. It will be null only if + * the user has not installed or logged in to the Facebook app. + */ + @Deprecated + public static String getAttributionId(ContentResolver contentResolver) { + return Settings.getAttributionId(contentResolver); + } + + /** + * Get the auto install publish setting. If true, an install event will be published during authorize(), unless + * it has occurred previously or the app does not have install attribution enabled on the application's developer + * config page. + *

+ * This method is deprecated. See {@link Facebook} and {@link Settings} for more info. + * + * @return a Boolean indicating whether installation of the app should be auto-published. + */ + @Deprecated + public boolean getShouldAutoPublishInstall() { + return Settings.getShouldAutoPublishInstall(); + } + + /** + * Sets whether auto publishing of installs will occur. + *

+ * This method is deprecated. See {@link Facebook} and {@link Settings} for more info. + * + * @param value a Boolean indicating whether installation of the app should be auto-published. + */ + @Deprecated + public void setShouldAutoPublishInstall(boolean value) { + Settings.setShouldAutoPublishInstall(value); + } + + /** + * Manually publish install attribution to the Facebook graph. Internally handles tracking repeat calls to prevent + * multiple installs being published to the graph. + *

+ * This method is deprecated. See {@link Facebook} and {@link Settings} for more info. + * + * @param context the current Android context + * @return Always false. Earlier versions of the API returned true if it was no longer necessary to call. + * Apps should ignore this value, but for compatibility we will return false to ensure repeat calls (and the + * underlying code will prevent duplicate network traffic). + */ + @Deprecated + public boolean publishInstall(final Context context) { + Settings.publishInstallAsync(context, mAppId); + return false; + } + + /** + * Callback interface for dialog requests. + *

+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED. + *

+ * All public members of this class are intentionally deprecated. + * New code should instead use + * {@link com.facebook.widget.WebDialog} + *

+ * Adding @Deprecated to this class causes warnings in other deprecated classes + * that reference this one. That is the only reason this entire class is not + * deprecated. + * + * @devDocDeprecated + */ + public static interface DialogListener { + + /** + * Called when a dialog completes. + * + * Executed by the thread that initiated the dialog. + * + * @param values + * Key-value string pairs extracted from the response. + */ + public void onComplete(Bundle values); + + /** + * Called when a Facebook responds to a dialog with an error. + * + * Executed by the thread that initiated the dialog. + * + */ + public void onFacebookError(FacebookError e); + + /** + * Called when a dialog has an error. + * + * Executed by the thread that initiated the dialog. + * + */ + public void onError(DialogError e); + + /** + * Called when a dialog is canceled by the user. + * + * Executed by the thread that initiated the dialog. + * + */ + public void onCancel(); + + } + + /** + * Callback interface for service requests. + *

+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED. + *

+ * All public members of this class are intentionally deprecated. + * New code should instead use + * {@link Session} to manage session state. + *

+ * Adding @Deprecated to this class causes warnings in other deprecated classes + * that reference this one. That is the only reason this entire class is not + * deprecated. + * + * @devDocDeprecated + */ + public static interface ServiceListener { + + /** + * Called when a service request completes. + * + * @param values + * Key-value string pairs extracted from the response. + */ + public void onComplete(Bundle values); + + /** + * Called when a Facebook server responds to the request with an error. + */ + public void onFacebookError(FacebookError e); + + /** + * Called when a Facebook Service responds to the request with an error. + */ + public void onError(Error e); + + } + + @Deprecated + public static final String FB_APP_SIGNATURE = + "30820268308201d102044a9c4610300d06092a864886f70d0101040500307a310" + + "b3009060355040613025553310b30090603550408130243413112301006035504" + + "07130950616c6f20416c746f31183016060355040a130f46616365626f6f6b204" + + "d6f62696c653111300f060355040b130846616365626f6f6b311d301b06035504" + + "03131446616365626f6f6b20436f72706f726174696f6e3020170d30393038333" + + "13231353231365a180f32303530303932353231353231365a307a310b30090603" + + "55040613025553310b30090603550408130243413112301006035504071309506" + + "16c6f20416c746f31183016060355040a130f46616365626f6f6b204d6f62696c" + + "653111300f060355040b130846616365626f6f6b311d301b06035504031314466" + + "16365626f6f6b20436f72706f726174696f6e30819f300d06092a864886f70d01" + + "0101050003818d0030818902818100c207d51df8eb8c97d93ba0c8c1002c928fa" + + "b00dc1b42fca5e66e99cc3023ed2d214d822bc59e8e35ddcf5f44c7ae8ade50d7" + + "e0c434f500e6c131f4a2834f987fc46406115de2018ebbb0d5a3c261bd97581cc" + + "fef76afc7135a6d59e8855ecd7eacc8f8737e794c60a761c536b72b11fac8e603" + + "f5da1a2d54aa103b8a13c0dbc10203010001300d06092a864886f70d010104050" + + "0038181005ee9be8bcbb250648d3b741290a82a1c9dc2e76a0af2f2228f1d9f9c" + + "4007529c446a70175c5a900d5141812866db46be6559e2141616483998211f4a6" + + "73149fb2232a10d247663b26a9031e15f84bc1c74d141ff98a02d76f85b2c8ab2" + + "571b6469b232d8e768a7f7ca04f7abe4a775615916c07940656b58717457b42bd" + + "928a2"; + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/FacebookError.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/FacebookError.java new file mode 100644 index 0000000000..41ae794fca --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/FacebookError.java @@ -0,0 +1,64 @@ +/** + * Copyright 2010-present Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +/** + * Encapsulation of a Facebook Error: a Facebook request that could not be + * fulfilled. + *

+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED. + *

+ * All public members of this class are intentionally deprecated. + * New code should instead use + * {@link com.facebook.FacebookException} + *

+ * Adding @Deprecated to this class causes warnings in other deprecated classes + * that reference this one. That is the only reason this entire class is not + * deprecated. + * + * @devDocDeprecated + */ +public class FacebookError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private int mErrorCode = 0; + private String mErrorType; + + @Deprecated + public FacebookError(String message) { + super(message); + } + + @Deprecated + public FacebookError(String message, String type, int code) { + super(message); + mErrorType = type; + mErrorCode = code; + } + + @Deprecated + public int getErrorCode() { + return mErrorCode; + } + + @Deprecated + public String getErrorType() { + return mErrorType; + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/FbDialog.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/FbDialog.java new file mode 100644 index 0000000000..603e69280e --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/FbDialog.java @@ -0,0 +1,83 @@ +/** + * Copyright 2010-present Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import android.content.Context; +import android.os.Bundle; +import com.facebook.*; +import com.facebook.android.Facebook.DialogListener; +import com.facebook.widget.WebDialog; + +/** + * This class is deprecated. See {@link com.facebook.widget.WebDialog}. + */ +@Deprecated +public class FbDialog extends WebDialog { + private DialogListener mListener; + + public FbDialog(Context context, String url, DialogListener listener) { + this(context, url, listener, DEFAULT_THEME); + } + + public FbDialog(Context context, String url, DialogListener listener, int theme) { + super(context, url, theme); + setDialogListener(listener); + } + + public FbDialog(Context context, String action, Bundle parameters, DialogListener listener) { + super(context, action, parameters, DEFAULT_THEME, null); + setDialogListener(listener); + } + + public FbDialog(Context context, String action, Bundle parameters, DialogListener listener, + int theme) { + super(context, action, parameters, theme, null); + setDialogListener(listener); + } + + private void setDialogListener(DialogListener listener) { + this.mListener = listener; + setOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(Bundle values, FacebookException error) { + callDialogListener(values, error); + } + }); + } + + private void callDialogListener(Bundle values, FacebookException error) { + if (mListener == null) { + return; + } + + if (values != null) { + mListener.onComplete(values); + } else { + if (error instanceof FacebookDialogException) { + FacebookDialogException facebookDialogException = (FacebookDialogException) error; + DialogError dialogError = new DialogError(facebookDialogException.getMessage(), + facebookDialogException.getErrorCode(), facebookDialogException.getFailingUrl()); + mListener.onError(dialogError); + } else if (error instanceof FacebookOperationCanceledException) { + mListener.onCancel(); + } else { + FacebookError facebookError = new FacebookError(error.getMessage()); + mListener.onFacebookError(facebookError); + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/Util.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/Util.java new file mode 100644 index 0000000000..4ece878dff --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/android/Util.java @@ -0,0 +1,314 @@ +/** + * Copyright 2010-present Facebook + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.android; + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.os.Bundle; +import com.facebook.internal.Utility; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.*; +import java.net.*; + +/** + * Utility class supporting the Facebook Object. + *

+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED. + *

+ * All public members of this class are intentionally deprecated. + * New code should instead use + * {@link com.facebook.Request} + *

+ * Adding @Deprecated to this class causes warnings in other deprecated classes + * that reference this one. That is the only reason this entire class is not + * deprecated. + * + * @devDocDeprecated + */ +public final class Util { + + private final static String UTF8 = "UTF-8"; + + /** + * Generate the multi-part post body providing the parameters and boundary + * string + * + * @param parameters the parameters need to be posted + * @param boundary the random string as boundary + * @return a string of the post body + */ + @Deprecated + public static String encodePostBody(Bundle parameters, String boundary) { + if (parameters == null) return ""; + StringBuilder sb = new StringBuilder(); + + for (String key : parameters.keySet()) { + Object parameter = parameters.get(key); + if (!(parameter instanceof String)) { + continue; + } + + sb.append("Content-Disposition: form-data; name=\"" + key + + "\"\r\n\r\n" + (String)parameter); + sb.append("\r\n" + "--" + boundary + "\r\n"); + } + + return sb.toString(); + } + + @Deprecated + public static String encodeUrl(Bundle parameters) { + if (parameters == null) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String key : parameters.keySet()) { + Object parameter = parameters.get(key); + if (!(parameter instanceof String)) { + continue; + } + + if (first) first = false; else sb.append("&"); + sb.append(URLEncoder.encode(key) + "=" + + URLEncoder.encode(parameters.getString(key))); + } + return sb.toString(); + } + + @Deprecated + public static Bundle decodeUrl(String s) { + Bundle params = new Bundle(); + if (s != null) { + String array[] = s.split("&"); + for (String parameter : array) { + String v[] = parameter.split("="); + + try { + if (v.length == 2) { + params.putString(URLDecoder.decode(v[0], UTF8), + URLDecoder.decode(v[1], UTF8)); + } else if (v.length == 1) { + params.putString(URLDecoder.decode(v[0], UTF8), ""); + } + } catch (UnsupportedEncodingException e) { + // shouldn't happen + } + } + } + return params; + } + + /** + * Parse a URL query and fragment parameters into a key-value bundle. + * + * @param url the URL to parse + * @return a dictionary bundle of keys and values + */ + @Deprecated + public static Bundle parseUrl(String url) { + // hack to prevent MalformedURLException + url = url.replace("fbconnect", "http"); + try { + URL u = new URL(url); + Bundle b = decodeUrl(u.getQuery()); + b.putAll(decodeUrl(u.getRef())); + return b; + } catch (MalformedURLException e) { + return new Bundle(); + } + } + + + /** + * Connect to an HTTP URL and return the response as a string. + * + * Note that the HTTP method override is used on non-GET requests. (i.e. + * requests are made as "POST" with method specified in the body). + * + * @param url - the resource to open: must be a welformed URL + * @param method - the HTTP method to use ("GET", "POST", etc.) + * @param params - the query parameter for the URL (e.g. access_token=foo) + * @return the URL contents as a String + * @throws MalformedURLException - if the URL format is invalid + * @throws IOException - if a network problem occurs + */ + @Deprecated + public static String openUrl(String url, String method, Bundle params) + throws MalformedURLException, IOException { + // random string as boundary for multi-part http post + String strBoundary = "3i2ndDfv2rTHiSisAbouNdArYfORhtTPEefj3q2f"; + String endLine = "\r\n"; + + OutputStream os; + + if (method.equals("GET")) { + url = url + "?" + encodeUrl(params); + } + Utility.logd("Facebook-Util", method + " URL: " + url); + HttpURLConnection conn = + (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestProperty("User-Agent", System.getProperties(). + getProperty("http.agent") + " FacebookAndroidSDK"); + if (!method.equals("GET")) { + Bundle dataparams = new Bundle(); + for (String key : params.keySet()) { + Object parameter = params.get(key); + if (parameter instanceof byte[]) { + dataparams.putByteArray(key, (byte[])parameter); + } + } + + // use method override + if (!params.containsKey("method")) { + params.putString("method", method); + } + + if (params.containsKey("access_token")) { + String decoded_token = + URLDecoder.decode(params.getString("access_token")); + params.putString("access_token", decoded_token); + } + + conn.setRequestMethod("POST"); + conn.setRequestProperty( + "Content-Type", + "multipart/form-data;boundary="+strBoundary); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.connect(); + + os = new BufferedOutputStream(conn.getOutputStream()); + + try { + os.write(("--" + strBoundary +endLine).getBytes()); + os.write((encodePostBody(params, strBoundary)).getBytes()); + os.write((endLine + "--" + strBoundary + endLine).getBytes()); + + if (!dataparams.isEmpty()) { + + for (String key: dataparams.keySet()){ + os.write(("Content-Disposition: form-data; filename=\"" + key + "\"" + endLine).getBytes()); + os.write(("Content-Type: content/unknown" + endLine + endLine).getBytes()); + os.write(dataparams.getByteArray(key)); + os.write((endLine + "--" + strBoundary + endLine).getBytes()); + + } + } + os.flush(); + } finally { + os.close(); + } + } + + String response = ""; + try { + response = read(conn.getInputStream()); + } catch (FileNotFoundException e) { + // Error Stream contains JSON that we can parse to a FB error + response = read(conn.getErrorStream()); + } + return response; + } + + @Deprecated + private static String read(InputStream in) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedReader r = new BufferedReader(new InputStreamReader(in), 1000); + for (String line = r.readLine(); line != null; line = r.readLine()) { + sb.append(line); + } + in.close(); + return sb.toString(); + } + + /** + * Parse a server response into a JSON Object. This is a basic + * implementation using org.json.JSONObject representation. More + * sophisticated applications may wish to do their own parsing. + * + * The parsed JSON is checked for a variety of error fields and + * a FacebookException is thrown if an error condition is set, + * populated with the error message and error type or code if + * available. + * + * @param response - string representation of the response + * @return the response as a JSON Object + * @throws JSONException - if the response is not valid JSON + * @throws FacebookError - if an error condition is set + */ + @Deprecated + public static JSONObject parseJson(String response) + throws JSONException, FacebookError { + // Edge case: when sending a POST request to /[post_id]/likes + // the return value is 'true' or 'false'. Unfortunately + // these values cause the JSONObject constructor to throw + // an exception. + if (response.equals("false")) { + throw new FacebookError("request failed"); + } + if (response.equals("true")) { + response = "{value : true}"; + } + JSONObject json = new JSONObject(response); + + // errors set by the server are not consistent + // they depend on the method and endpoint + if (json.has("error")) { + JSONObject error = json.getJSONObject("error"); + throw new FacebookError( + error.getString("message"), error.getString("type"), 0); + } + if (json.has("error_code") && json.has("error_msg")) { + throw new FacebookError(json.getString("error_msg"), "", + Integer.parseInt(json.getString("error_code"))); + } + if (json.has("error_code")) { + throw new FacebookError("request failed", "", + Integer.parseInt(json.getString("error_code"))); + } + if (json.has("error_msg")) { + throw new FacebookError(json.getString("error_msg")); + } + if (json.has("error_reason")) { + throw new FacebookError(json.getString("error_reason")); + } + return json; + } + + /** + * Display a simple alert dialog with the given text and title. + * + * @param context + * Android context in which the dialog should be displayed + * @param title + * Alert dialog title + * @param text + * Alert dialog message + */ + @Deprecated + public static void showAlert(Context context, String title, String text) { + Builder alertBuilder = new Builder(context); + alertBuilder.setTitle(title); + alertBuilder.setMessage(text); + alertBuilder.create().show(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/AnalyticsEvents.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/AnalyticsEvents.java new file mode 100644 index 0000000000..e57ec0fb7d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/AnalyticsEvents.java @@ -0,0 +1,39 @@ +package com.facebook.internal; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public class AnalyticsEvents { + public static final String EVENT_NATIVE_LOGIN_DIALOG_COMPLETE = "fb_dialogs_native_login_dialog_complete"; + public static final String EVENT_NATIVE_LOGIN_DIALOG_START = "fb_dialogs_native_login_dialog_start"; + public static final String EVENT_WEB_LOGIN_COMPLETE = "fb_dialogs_web_login_dialog_complete"; + public static final String EVENT_FRIEND_PICKER_USAGE = "fb_friend_picker_usage"; + public static final String EVENT_PLACE_PICKER_USAGE = "fb_place_picker_usage"; + public static final String EVENT_LOGIN_VIEW_USAGE = "fb_login_view_usage"; + public static final String EVENT_USER_SETTINGS_USAGE = "fb_user_settings_vc_usage"; + public static final String EVENT_NATIVE_DIALOG_START = "fb_native_dialog_start"; + public static final String EVENT_NATIVE_DIALOG_COMPLETE = "fb_native_dialog_complete"; + + public static final String PARAMETER_WEB_LOGIN_E2E = "fb_web_login_e2e"; + public static final String PARAMETER_WEB_LOGIN_SWITCHBACK_TIME = "fb_web_login_switchback_time"; + public static final String PARAMETER_APP_ID = "app_id"; + public static final String PARAMETER_ACTION_ID = "action_id"; + public static final String PARAMETER_NATIVE_LOGIN_DIALOG_START_TIME = "fb_native_login_dialog_start_time"; + public static final String PARAMETER_NATIVE_LOGIN_DIALOG_COMPLETE_TIME = + "fb_native_login_dialog_complete_time"; + + public static final String PARAMETER_DIALOG_OUTCOME = "fb_dialog_outcome"; + public static final String PARAMETER_DIALOG_OUTCOME_VALUE_COMPLETED = "Completed"; + public static final String PARAMETER_DIALOG_OUTCOME_VALUE_UNKNOWN = "Unknown"; + public static final String PARAMETER_DIALOG_OUTCOME_VALUE_CANCELLED = "Cancelled"; + public static final String PARAMETER_DIALOG_OUTCOME_VALUE_FAILED = "Failed"; + + public static final String EVENT_NATIVE_DIALOG_TYPE_SHARE = "fb_dialogs_present_share"; + public static final String EVENT_NATIVE_DIALOG_TYPE_MESSAGE = "fb_dialogs_present_message"; + public static final String EVENT_NATIVE_DIALOG_TYPE_OG_SHARE = "fb_dialogs_present_share_og"; + public static final String EVENT_NATIVE_DIALOG_TYPE_OG_MESSAGE = "fb_dialogs_present_message_og"; + public static final String EVENT_NATIVE_DIALOG_TYPE_PHOTO_SHARE = "fb_dialogs_present_share_photo"; + public static final String EVENT_NATIVE_DIALOG_TYPE_PHOTO_MESSAGE = "fb_dialogs_present_message_photo"; +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/AttributionIdentifiers.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/AttributionIdentifiers.java new file mode 100644 index 0000000000..1c8f63018d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/AttributionIdentifiers.java @@ -0,0 +1,144 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import java.lang.reflect.Method; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public class AttributionIdentifiers { + private static final String TAG = AttributionIdentifiers.class.getCanonicalName(); + private static final Uri ATTRIBUTION_ID_CONTENT_URI = + Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider"); + private static final String ATTRIBUTION_ID_COLUMN_NAME = "aid"; + private static final String ANDROID_ID_COLUMN_NAME = "androidid"; + private static final String LIMIT_TRACKING_COLUMN_NAME = "limit_tracking"; + + // com.google.android.gms.common.ConnectionResult.SUCCESS + private static final int CONNECTION_RESULT_SUCCESS = 0; + + private static final long IDENTIFIER_REFRESH_INTERVAL_MILLIS = 3600 * 1000; + + private String attributionId; + private String androidAdvertiserId; + private boolean limitTracking; + private long fetchTime; + + private static AttributionIdentifiers recentlyFetchedIdentifiers; + + private static AttributionIdentifiers getAndroidId(Context context) { + AttributionIdentifiers identifiers = new AttributionIdentifiers(); + try { + Method isGooglePlayServicesAvailable = Utility.getMethodQuietly( + "com.google.android.gms.common.GooglePlayServicesUtil", + "isGooglePlayServicesAvailable", + Context.class + ); + + if (isGooglePlayServicesAvailable == null) { + return identifiers; + } + + Object connectionResult = Utility.invokeMethodQuietly(null, isGooglePlayServicesAvailable, context); + if (!(connectionResult instanceof Integer) || (Integer) connectionResult != CONNECTION_RESULT_SUCCESS) { + return identifiers; + } + + Method getAdvertisingIdInfo = Utility.getMethodQuietly( + "com.google.android.gms.ads.identifier.AdvertisingIdClient", + "getAdvertisingIdInfo", + Context.class + ); + if (getAdvertisingIdInfo == null) { + return identifiers; + } + Object advertisingInfo = Utility.invokeMethodQuietly(null, getAdvertisingIdInfo, context); + if (advertisingInfo == null) { + return identifiers; + } + + Method getId = Utility.getMethodQuietly(advertisingInfo.getClass(), "getId"); + Method isLimitAdTrackingEnabled = Utility.getMethodQuietly(advertisingInfo.getClass(), "isLimitAdTrackingEnabled"); + if (getId == null || isLimitAdTrackingEnabled == null) { + return identifiers; + } + + identifiers.androidAdvertiserId = (String) Utility.invokeMethodQuietly(advertisingInfo, getId); + identifiers.limitTracking = (Boolean) Utility.invokeMethodQuietly(advertisingInfo, isLimitAdTrackingEnabled); + } catch (Exception e) { + Utility.logd("android_id", e); + } + return identifiers; + } + + public static AttributionIdentifiers getAttributionIdentifiers(Context context) { + if (recentlyFetchedIdentifiers != null && + System.currentTimeMillis() - recentlyFetchedIdentifiers.fetchTime < IDENTIFIER_REFRESH_INTERVAL_MILLIS) { + return recentlyFetchedIdentifiers; + } + + AttributionIdentifiers identifiers = getAndroidId(context); + + try { + String [] projection = {ATTRIBUTION_ID_COLUMN_NAME, ANDROID_ID_COLUMN_NAME, LIMIT_TRACKING_COLUMN_NAME}; + Cursor c = context.getContentResolver().query(ATTRIBUTION_ID_CONTENT_URI, projection, null, null, null); + if (c == null || !c.moveToFirst()) { + return null; + } + int attributionColumnIndex = c.getColumnIndex(ATTRIBUTION_ID_COLUMN_NAME); + int androidIdColumnIndex = c.getColumnIndex(ANDROID_ID_COLUMN_NAME); + int limitTrackingColumnIndex = c.getColumnIndex(LIMIT_TRACKING_COLUMN_NAME); + + identifiers.attributionId = c.getString(attributionColumnIndex); + + // if we failed to call Google's APIs directly (due to improper integration by the client), it may be + // possible for the local facebook application to relay it to us. + if (androidIdColumnIndex > 0 && limitTrackingColumnIndex > 0 && identifiers.getAndroidAdvertiserId() == null) { + identifiers.androidAdvertiserId = c.getString(androidIdColumnIndex); + identifiers.limitTracking = Boolean.parseBoolean(c.getString(limitTrackingColumnIndex)); + } + c.close(); + } catch (Exception e) { + Log.d(TAG, "Caught unexpected exception in getAttributionId(): " + e.toString()); + return null; + } + + identifiers.fetchTime = System.currentTimeMillis(); + recentlyFetchedIdentifiers = identifiers; + return identifiers; + } + + public String getAttributionId() { + return attributionId; + } + + public String getAndroidAdvertiserId() { + return androidAdvertiserId; + } + + public boolean isTrackingLimited() { + return limitTracking; + } +} \ No newline at end of file diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/CacheableRequestBatch.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/CacheableRequestBatch.java new file mode 100644 index 0000000000..83e2d2f44d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/CacheableRequestBatch.java @@ -0,0 +1,57 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import com.facebook.Request; +import com.facebook.RequestBatch; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public class CacheableRequestBatch extends RequestBatch { + private String cacheKey; + private boolean forceRoundTrip; + + public CacheableRequestBatch() { + } + + public CacheableRequestBatch(Request... requests) { + super(requests); + } + + public final String getCacheKeyOverride() { + return cacheKey; + } + + // If this is set, the provided string will override the default key (the URL) for single requests. + // There is no default for multi-request batches, so no caching will be done unless the override is + // specified. + public final void setCacheKeyOverride(String cacheKey) { + this.cacheKey = cacheKey; + } + + public final boolean getForceRoundTrip() { + return forceRoundTrip; + } + + public final void setForceRoundTrip(boolean forceRoundTrip) { + this.forceRoundTrip = forceRoundTrip; + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/FileLruCache.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/FileLruCache.java new file mode 100644 index 0000000000..0f23176b9f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/FileLruCache.java @@ -0,0 +1,648 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.util.Log; +import com.facebook.LoggingBehavior; +import com.facebook.Settings; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.*; +import java.security.InvalidParameterException; +import java.util.Date; +import java.util.PriorityQueue; +import java.util.concurrent.atomic.AtomicLong; + +// This class is intended to be thread-safe. +// +// There are two classes of files: buffer files and cache files: +// - A buffer file is in the process of being written, and there is an open stream on the file. These files are +// named as "bufferN" where N is an incrementing integer. On startup, we delete all existing files of this form. +// Once the stream is closed, we rename the buffer file to a cache file or attempt to delete if this fails. We +// do not otherwise ever attempt to delete these files. +// - A cache file is a non-changing file that is named by the md5 hash of the cache key. We monitor the size of +// these files in aggregate and remove the oldest one(s) to stay under quota. This process does not block threads +// calling into this class, so theoretically we could go arbitrarily over quota but in practice this should not +// happen because deleting files should be much cheaper than downloading new file content. +// +// Since there can only ever be one thread accessing a particular buffer file, we do not synchronize access to these. +// We do assume that file rename is atomic when converting a buffer file to a cache file, and that if multiple files +// are renamed to a single target that exactly one of them continues to exist. +// +// Standard POSIX file semantics guarantee being able to continue to use a file handle even after the +// corresponding file has been deleted. Given this and that cache files never change other than deleting in trim() +// or clear(), we only have to ensure that there is at most one trim() or clear() process deleting files at any +// given time. + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public final class FileLruCache { + static final String TAG = FileLruCache.class.getSimpleName(); + private static final String HEADER_CACHEKEY_KEY = "key"; + private static final String HEADER_CACHE_CONTENT_TAG_KEY = "tag"; + + private static final AtomicLong bufferIndex = new AtomicLong(); + + private final String tag; + private final Limits limits; + private final File directory; + private boolean isTrimPending; + private boolean isTrimInProgress; + private final Object lock; + private AtomicLong lastClearCacheTime = new AtomicLong(0); + + // The value of tag should be a final String that works as a directory name. + public FileLruCache(Context context, String tag, Limits limits) { + this.tag = tag; + this.limits = limits; + this.directory = new File(context.getCacheDir(), tag); + this.lock = new Object(); + + // Ensure the cache dir exists + if (this.directory.mkdirs() || this.directory.isDirectory()) { + // Remove any stale partially-written files from a previous run + BufferFile.deleteAll(this.directory); + } + } + + // This is not robust to files changing dynamically underneath it and should therefore only be used + // for test code. If we ever need this for product code we need to think through synchronization. + // See the threading notes at the top of this class. + // + // Also, since trim() runs asynchronously now, this blocks until any pending trim has completed. + long sizeInBytesForTest() { + synchronized (lock) { + while (isTrimPending || isTrimInProgress) { + try { + lock.wait(); + } catch (InterruptedException e) { + // intentional no-op + } + } + } + + File[] files = this.directory.listFiles(); + long total = 0; + if (files != null) { + for (File file : files) { + total += file.length(); + } + } + return total; + } + + public InputStream get(String key) throws IOException { + return get(key, null); + } + + public InputStream get(String key, String contentTag) throws IOException { + File file = new File(this.directory, Utility.md5hash(key)); + + FileInputStream input = null; + try { + input = new FileInputStream(file); + } catch (IOException e) { + return null; + } + + BufferedInputStream buffered = new BufferedInputStream(input, Utility.DEFAULT_STREAM_BUFFER_SIZE); + boolean success = false; + + try { + JSONObject header = StreamHeader.readHeader(buffered); + if (header == null) { + return null; + } + + String foundKey = header.optString(HEADER_CACHEKEY_KEY); + if ((foundKey == null) || !foundKey.equals(key)) { + return null; + } + + String headerContentTag = header.optString(HEADER_CACHE_CONTENT_TAG_KEY, null); + + if ((contentTag == null && headerContentTag != null) || + (contentTag != null && !contentTag.equals(headerContentTag))) { + return null; + } + + long accessTime = new Date().getTime(); + Logger.log(LoggingBehavior.CACHE, TAG, "Setting lastModified to " + Long.valueOf(accessTime) + " for " + + file.getName()); + file.setLastModified(accessTime); + + success = true; + return buffered; + } finally { + if (!success) { + buffered.close(); + } + } + } + + OutputStream openPutStream(final String key) throws IOException { + return openPutStream(key, null); + } + + public OutputStream openPutStream(final String key, String contentTag) throws IOException { + final File buffer = BufferFile.newFile(this.directory); + buffer.delete(); + if (!buffer.createNewFile()) { + throw new IOException("Could not create file at " + buffer.getAbsolutePath()); + } + + FileOutputStream file = null; + try { + file = new FileOutputStream(buffer); + } catch (FileNotFoundException e) { + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating buffer output stream: " + e); + throw new IOException(e.getMessage()); + } + + final long bufferFileCreateTime = System.currentTimeMillis(); + StreamCloseCallback renameToTargetCallback = new StreamCloseCallback() { + @Override + public void onClose() { + // if the buffer file was created before the cache was cleared, then the buffer file + // should be deleted rather than renamed and saved. + if (bufferFileCreateTime < lastClearCacheTime.get()) { + buffer.delete(); + } else { + renameToTargetAndTrim(key, buffer); + } + } + }; + + CloseCallbackOutputStream cleanup = new CloseCallbackOutputStream(file, renameToTargetCallback); + BufferedOutputStream buffered = new BufferedOutputStream(cleanup, Utility.DEFAULT_STREAM_BUFFER_SIZE); + boolean success = false; + + try { + // Prefix the stream with the actual key, since there could be collisions + JSONObject header = new JSONObject(); + header.put(HEADER_CACHEKEY_KEY, key); + if (!Utility.isNullOrEmpty(contentTag)) { + header.put(HEADER_CACHE_CONTENT_TAG_KEY, contentTag); + } + + StreamHeader.writeHeader(buffered, header); + + success = true; + return buffered; + } catch (JSONException e) { + // JSON is an implementation detail of the cache, so don't let JSON exceptions out. + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating JSON header for cache file: " + e); + throw new IOException(e.getMessage()); + } finally { + if (!success) { + buffered.close(); + } + } + } + + public void clearCache() { + // get the current directory listing of files to delete + final File[] filesToDelete = directory.listFiles(BufferFile.excludeBufferFiles()); + lastClearCacheTime.set(System.currentTimeMillis()); + if (filesToDelete != null) { + Settings.getExecutor().execute(new Runnable() { + @Override + public void run() { + for (File file : filesToDelete) { + file.delete(); + } + } + }); + } + } + + private void renameToTargetAndTrim(String key, File buffer) { + final File target = new File(directory, Utility.md5hash(key)); + + // This is triggered by close(). By the time close() returns, the file should be cached, so this needs to + // happen synchronously on this thread. + // + // However, it does not need to be synchronized, since in the race we will just start an unnecesary trim + // operation. Avoiding the cost of holding the lock across the file operation seems worth this cost. + if (!buffer.renameTo(target)) { + buffer.delete(); + } + + postTrim(); + } + + // Opens an output stream for the key, and creates an input stream wrapper to copy + // the contents of input into the new output stream. The effect is to store a + // copy of input, and associate that data with key. + public InputStream interceptAndPut(String key, InputStream input) throws IOException { + OutputStream output = openPutStream(key); + return new CopyingInputStream(input, output); + } + + public String toString() { + return "{FileLruCache:" + " tag:" + this.tag + " file:" + this.directory.getName() + "}"; + } + + private void postTrim() { + synchronized (lock) { + if (!isTrimPending) { + isTrimPending = true; + Settings.getExecutor().execute(new Runnable() { + @Override + public void run() { + trim(); + } + }); + } + } + } + + private void trim() { + synchronized (lock) { + isTrimPending = false; + isTrimInProgress = true; + } + try { + Logger.log(LoggingBehavior.CACHE, TAG, "trim started"); + PriorityQueue heap = new PriorityQueue(); + long size = 0; + long count = 0; + File[] filesToTrim =this.directory.listFiles(BufferFile.excludeBufferFiles()); + if (filesToTrim != null) { + for (File file : filesToTrim) { + ModifiedFile modified = new ModifiedFile(file); + heap.add(modified); + Logger.log(LoggingBehavior.CACHE, TAG, " trim considering time=" + Long.valueOf(modified.getModified()) + + " name=" + modified.getFile().getName()); + + size += file.length(); + count++; + } + } + + while ((size > limits.getByteCount()) || (count > limits.getFileCount())) { + File file = heap.remove().getFile(); + Logger.log(LoggingBehavior.CACHE, TAG, " trim removing " + file.getName()); + size -= file.length(); + count--; + file.delete(); + } + } finally { + synchronized (lock) { + isTrimInProgress = false; + lock.notifyAll(); + } + } + } + + private static class BufferFile { + private static final String FILE_NAME_PREFIX = "buffer"; + private static final FilenameFilter filterExcludeBufferFiles = new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return !filename.startsWith(FILE_NAME_PREFIX); + } + }; + private static final FilenameFilter filterExcludeNonBufferFiles = new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(FILE_NAME_PREFIX); + } + }; + + static void deleteAll(final File root) { + File[] filesToDelete = root.listFiles(excludeNonBufferFiles()); + if (filesToDelete != null) { + for (File file : filesToDelete) { + file.delete(); + } + } + } + + static FilenameFilter excludeBufferFiles() { + return filterExcludeBufferFiles; + } + + static FilenameFilter excludeNonBufferFiles() { + return filterExcludeNonBufferFiles; + } + + static File newFile(final File root) { + String name = FILE_NAME_PREFIX + Long.valueOf(bufferIndex.incrementAndGet()).toString(); + return new File(root, name); + } + } + + // Treats the first part of a stream as a header, reads/writes it as a JSON blob, and + // leaves the stream positioned exactly after the header. + // + // The format is as follows: + // byte: meaning + // --------------------------------- + // 0: version number + // 1-3: big-endian JSON header blob size + // 4-size+4: UTF-8 JSON header blob + // ...: stream data + private static final class StreamHeader { + private static final int HEADER_VERSION = 0; + + static void writeHeader(OutputStream stream, JSONObject header) throws IOException { + String headerString = header.toString(); + byte[] headerBytes = headerString.getBytes(); + + // Write version number and big-endian header size + stream.write(HEADER_VERSION); + stream.write((headerBytes.length >> 16) & 0xff); + stream.write((headerBytes.length >> 8) & 0xff); + stream.write((headerBytes.length >> 0) & 0xff); + + stream.write(headerBytes); + } + + static JSONObject readHeader(InputStream stream) throws IOException { + int version = stream.read(); + if (version != HEADER_VERSION) { + return null; + } + + int headerSize = 0; + for (int i = 0; i < 3; i++) { + int b = stream.read(); + if (b == -1) { + Logger.log(LoggingBehavior.CACHE, TAG, + "readHeader: stream.read returned -1 while reading header size"); + return null; + } + headerSize <<= 8; + headerSize += b & 0xff; + } + + byte[] headerBytes = new byte[headerSize]; + int count = 0; + while (count < headerBytes.length) { + int readCount = stream.read(headerBytes, count, headerBytes.length - count); + if (readCount < 1) { + Logger.log(LoggingBehavior.CACHE, TAG, + "readHeader: stream.read stopped at " + Integer.valueOf(count) + " when expected " + + headerBytes.length); + return null; + } + count += readCount; + } + + String headerString = new String(headerBytes); + JSONObject header = null; + JSONTokener tokener = new JSONTokener(headerString); + try { + Object parsed = tokener.nextValue(); + if (!(parsed instanceof JSONObject)) { + Logger.log(LoggingBehavior.CACHE, TAG, "readHeader: expected JSONObject, got " + parsed.getClass().getCanonicalName()); + return null; + } + header = (JSONObject) parsed; + } catch (JSONException e) { + throw new IOException(e.getMessage()); + } + + return header; + } + } + + private static class CloseCallbackOutputStream extends OutputStream { + final OutputStream innerStream; + final StreamCloseCallback callback; + + CloseCallbackOutputStream(OutputStream innerStream, StreamCloseCallback callback) { + this.innerStream = innerStream; + this.callback = callback; + } + + @Override + public void close() throws IOException { + try { + this.innerStream.close(); + } finally { + this.callback.onClose(); + } + } + + @Override + public void flush() throws IOException { + this.innerStream.flush(); + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + this.innerStream.write(buffer, offset, count); + } + + @Override + public void write(byte[] buffer) throws IOException { + this.innerStream.write(buffer); + } + + @Override + public void write(int oneByte) throws IOException { + this.innerStream.write(oneByte); + } + } + + private static final class CopyingInputStream extends InputStream { + final InputStream input; + final OutputStream output; + + CopyingInputStream(final InputStream input, final OutputStream output) { + this.input = input; + this.output = output; + } + + @Override + public int available() throws IOException { + return input.available(); + } + + @Override + public void close() throws IOException { + // According to http://www.cs.cornell.edu/andru/javaspec/11.doc.html: + // "If a finally clause is executed because of abrupt completion of a try block and the finally clause + // itself completes abruptly, then the reason for the abrupt completion of the try block is discarded + // and the new reason for abrupt completion is propagated from there." + // + // Android does appear to behave like this. + try { + this.input.close(); + } finally { + this.output.close(); + } + } + + @Override + public void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read(byte[] buffer) throws IOException { + int count = input.read(buffer); + if (count > 0) { + output.write(buffer, 0, count); + } + return count; + } + + @Override + public int read() throws IOException { + int b = input.read(); + if (b >= 0) { + output.write(b); + } + return b; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + int count = input.read(buffer, offset, length); + if (count > 0) { + output.write(buffer, offset, count); + } + return count; + } + + @Override + public synchronized void reset() { + throw new UnsupportedOperationException(); + } + + @Override + public long skip(long byteCount) throws IOException { + byte[] buffer = new byte[1024]; + long total = 0; + while (total < byteCount) { + int count = read(buffer, 0, (int)Math.min(byteCount - total, buffer.length)); + if (count < 0) { + return total; + } + total += count; + } + return total; + } + } + + public static final class Limits { + private int byteCount; + private int fileCount; + + public Limits() { + // A Samsung Galaxy Nexus can create 1k files in half a second. By the time + // it gets to 5k files it takes 5 seconds. 10k files took 15 seconds. This + // continues to slow down as files are added. This assumes all files are in + // a single directory. + // + // Following a git-like strategy where we partition MD5-named files based on + // the first 2 characters is slower across the board. + this.fileCount = 1024; + this.byteCount = 1024 * 1024; + } + + int getByteCount() { + return byteCount; + } + + int getFileCount() { + return fileCount; + } + + void setByteCount(int n) { + if (n < 0) { + throw new InvalidParameterException("Cache byte-count limit must be >= 0"); + } + byteCount = n; + } + + void setFileCount(int n) { + if (n < 0) { + throw new InvalidParameterException("Cache file count limit must be >= 0"); + } + fileCount = n; + } + } + + // Caches the result of lastModified during sort/heap operations + private final static class ModifiedFile implements Comparable { + private static final int HASH_SEED = 29; // Some random prime number + private static final int HASH_MULTIPLIER = 37; // Some random prime number + + private final File file; + private final long modified; + + ModifiedFile(File file) { + this.file = file; + this.modified = file.lastModified(); + } + + File getFile() { + return file; + } + + long getModified() { + return modified; + } + + @Override + public int compareTo(ModifiedFile another) { + if (getModified() < another.getModified()) { + return -1; + } else if (getModified() > another.getModified()) { + return 1; + } else { + return getFile().compareTo(another.getFile()); + } + } + + @Override + public boolean equals(Object another) { + return + (another instanceof ModifiedFile) && + (compareTo((ModifiedFile)another) == 0); + } + + @Override + public int hashCode() { + int result = HASH_SEED; + + result = (result * HASH_MULTIPLIER) + file.hashCode(); + result = (result * HASH_MULTIPLIER) + (int) (modified % Integer.MAX_VALUE); + + return result; + } + } + + private interface StreamCloseCallback { + void onClose(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageDownloader.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageDownloader.java new file mode 100644 index 0000000000..dc55fb9171 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageDownloader.java @@ -0,0 +1,360 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.Looper; +import com.facebook.FacebookException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.*; + +public class ImageDownloader { + private static final int DOWNLOAD_QUEUE_MAX_CONCURRENT = WorkQueue.DEFAULT_MAX_CONCURRENT; + private static final int CACHE_READ_QUEUE_MAX_CONCURRENT = 2; + private static Handler handler; + private static WorkQueue downloadQueue = new WorkQueue(DOWNLOAD_QUEUE_MAX_CONCURRENT); + private static WorkQueue cacheReadQueue = new WorkQueue(CACHE_READ_QUEUE_MAX_CONCURRENT); + + private static final Map pendingRequests = new HashMap(); + + /** + * Downloads the image specified in the passed in request. + * If a callback is specified, it is guaranteed to be invoked on the calling thread. + * @param request Request to process + */ + public static void downloadAsync(ImageRequest request) { + if (request == null) { + return; + } + + // NOTE: This is the ONLY place where the original request's Url is read. From here on, + // we will keep track of the Url separately. This is because we might be dealing with a + // redirect response and the Url might change. We can't create our own new ImageRequests + // for these changed Urls since the caller might be doing some book-keeping with the request's + // object reference. So we keep the old references and just map them to new urls in the downloader + RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag()); + synchronized (pendingRequests) { + DownloaderContext downloaderContext = pendingRequests.get(key); + if (downloaderContext != null) { + downloaderContext.request = request; + downloaderContext.isCancelled = false; + downloaderContext.workItem.moveToFront(); + } else { + enqueueCacheRead(request, key, request.isCachedRedirectAllowed()); + } + } + } + + public static boolean cancelRequest(ImageRequest request) { + boolean cancelled = false; + RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag()); + synchronized (pendingRequests) { + DownloaderContext downloaderContext = pendingRequests.get(key); + if (downloaderContext != null) { + // If we were able to find the request in our list of pending requests, then we will + // definitely be able to prevent an ImageResponse from being issued. This is regardless + // of whether a cache-read or network-download is underway for this request. + cancelled = true; + + if (downloaderContext.workItem.cancel()) { + pendingRequests.remove(key); + } else { + // May be attempting a cache-read right now. So keep track of the cancellation + // to prevent network calls etc + downloaderContext.isCancelled = true; + } + } + } + + return cancelled; + } + + public static void prioritizeRequest(ImageRequest request) { + RequestKey key = new RequestKey(request.getImageUri(), request.getCallerTag()); + synchronized (pendingRequests) { + DownloaderContext downloaderContext = pendingRequests.get(key); + if (downloaderContext != null) { + downloaderContext.workItem.moveToFront(); + } + } + } + + public static void clearCache(Context context) { + ImageResponseCache.clearCache(context); + UrlRedirectCache.clearCache(context); + } + + private static void enqueueCacheRead(ImageRequest request, RequestKey key, boolean allowCachedRedirects) { + enqueueRequest( + request, + key, + cacheReadQueue, + new CacheReadWorkItem(request.getContext(), key, allowCachedRedirects)); + } + + private static void enqueueDownload(ImageRequest request, RequestKey key) { + enqueueRequest( + request, + key, + downloadQueue, + new DownloadImageWorkItem(request.getContext(), key)); + } + + private static void enqueueRequest( + ImageRequest request, + RequestKey key, + WorkQueue workQueue, + Runnable workItem) { + synchronized (pendingRequests) { + DownloaderContext downloaderContext = new DownloaderContext(); + downloaderContext.request = request; + pendingRequests.put(key, downloaderContext); + + // The creation of the WorkItem should be done after the pending request has been registered. + // This is necessary since the WorkItem might kick off right away and attempt to retrieve + // the request's DownloaderContext prior to it being ready for access. + // + // It is also necessary to hold on to the lock until after the workItem is created, since + // calls to cancelRequest or prioritizeRequest might come in and expect a registered + // request to have a workItem available as well. + downloaderContext.workItem = workQueue.addActiveWorkItem(workItem); + } + } + + private static void issueResponse( + RequestKey key, + final Exception error, + final Bitmap bitmap, + final boolean isCachedRedirect) { + // Once the old downloader context is removed, we are thread-safe since this is the + // only reference to it + DownloaderContext completedRequestContext = removePendingRequest(key); + if (completedRequestContext != null && !completedRequestContext.isCancelled) { + final ImageRequest request = completedRequestContext.request; + final ImageRequest.Callback callback = request.getCallback(); + if (callback != null) { + getHandler().post(new Runnable() { + @Override + public void run() { + ImageResponse response = new ImageResponse( + request, + error, + isCachedRedirect, + bitmap); + callback.onCompleted(response); + } + }); + } + } + } + + private static void readFromCache(RequestKey key, Context context, boolean allowCachedRedirects) { + InputStream cachedStream = null; + boolean isCachedRedirect = false; + if (allowCachedRedirects) { + URI redirectUri = UrlRedirectCache.getRedirectedUri(context, key.uri); + if (redirectUri != null) { + cachedStream = ImageResponseCache.getCachedImageStream(redirectUri, context); + isCachedRedirect = cachedStream != null; + } + } + + if (!isCachedRedirect) { + cachedStream = ImageResponseCache.getCachedImageStream(key.uri, context); + } + + if (cachedStream != null) { + // We were able to find a cached image. + Bitmap bitmap = BitmapFactory.decodeStream(cachedStream); + Utility.closeQuietly(cachedStream); + issueResponse(key, null, bitmap, isCachedRedirect); + } else { + // Once the old downloader context is removed, we are thread-safe since this is the + // only reference to it + DownloaderContext downloaderContext = removePendingRequest(key); + if (downloaderContext != null && !downloaderContext.isCancelled) { + enqueueDownload(downloaderContext.request, key); + } + } + } + + private static void download(RequestKey key, Context context) { + HttpURLConnection connection = null; + InputStream stream = null; + Exception error = null; + Bitmap bitmap = null; + boolean issueResponse = true; + + try { + URL url = new URL(key.uri.toString()); + connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + + switch (connection.getResponseCode()) { + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + // redirect. So we need to perform further requests + issueResponse = false; + + String redirectLocation = connection.getHeaderField("location"); + if (!Utility.isNullOrEmpty(redirectLocation)) { + URI redirectUri = new URI(redirectLocation); + UrlRedirectCache.cacheUriRedirect(context, key.uri, redirectUri); + + // Once the old downloader context is removed, we are thread-safe since this is the + // only reference to it + DownloaderContext downloaderContext = removePendingRequest(key); + if (downloaderContext != null && !downloaderContext.isCancelled) { + enqueueCacheRead( + downloaderContext.request, + new RequestKey(redirectUri, key.tag), + false); + } + } + break; + + case HttpURLConnection.HTTP_OK: + // image should be available + stream = ImageResponseCache.interceptAndCacheImageStream(context, connection); + bitmap = BitmapFactory.decodeStream(stream); + break; + + default: + stream = connection.getErrorStream(); + InputStreamReader reader = new InputStreamReader(stream); + char[] buffer = new char[128]; + int bufferLength; + StringBuilder errorMessageBuilder = new StringBuilder(); + while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) { + errorMessageBuilder.append(buffer, 0, bufferLength); + } + Utility.closeQuietly(reader); + + error = new FacebookException(errorMessageBuilder.toString()); + break; + } + } catch (IOException e) { + error = e; + } catch (URISyntaxException e) { + error = e; + } finally { + Utility.closeQuietly(stream); + Utility.disconnectQuietly(connection); + } + + if (issueResponse) { + issueResponse(key, error, bitmap, false); + } + } + + private static synchronized Handler getHandler() { + if (handler == null) { + handler = new Handler(Looper.getMainLooper()); + } + return handler; + } + + private static DownloaderContext removePendingRequest(RequestKey key) { + synchronized (pendingRequests) { + return pendingRequests.remove(key); + } + } + + private static class RequestKey { + private static final int HASH_SEED = 29; // Some random prime number + private static final int HASH_MULTIPLIER = 37; // Some random prime number + + URI uri; + Object tag; + + RequestKey(URI url, Object tag) { + this.uri = url; + this.tag = tag; + } + + @Override + public int hashCode() { + int result = HASH_SEED; + + result = (result * HASH_MULTIPLIER) + uri.hashCode(); + result = (result * HASH_MULTIPLIER) + tag.hashCode(); + + return result; + } + + @Override + public boolean equals(Object o) { + boolean isEqual = false; + + if (o != null && o instanceof RequestKey) { + RequestKey compareTo = (RequestKey)o; + isEqual = compareTo.uri == uri && compareTo.tag == tag; + } + + return isEqual; + } + } + + private static class DownloaderContext { + WorkQueue.WorkItem workItem; + ImageRequest request; + boolean isCancelled; + } + + private static class CacheReadWorkItem implements Runnable { + private Context context; + private RequestKey key; + private boolean allowCachedRedirects; + + CacheReadWorkItem(Context context, RequestKey key, boolean allowCachedRedirects) { + this.context = context; + this.key = key; + this.allowCachedRedirects = allowCachedRedirects; + } + + @Override + public void run() { + readFromCache(key, context, allowCachedRedirects); + } + } + + private static class DownloadImageWorkItem implements Runnable { + private Context context; + private RequestKey key; + + DownloadImageWorkItem(Context context, RequestKey key) { + this.context = context; + this.key = key; + } + + @Override + public void run() { + download(key, context); + } + + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageRequest.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageRequest.java new file mode 100644 index 0000000000..2c84bf88f6 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageRequest.java @@ -0,0 +1,144 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.net.Uri; + +import java.net.URI; +import java.net.URISyntaxException; + +public class ImageRequest { + + public interface Callback { + /** + * This method should always be called on the UI thread. ImageDownloader makes + * sure to do this when it is responsible for issuing the ImageResponse + * @param response + */ + void onCompleted(ImageResponse response); + } + + public static final int UNSPECIFIED_DIMENSION = 0; + + private static final String PROFILEPIC_URL_FORMAT = + "https://graph.facebook.com/%s/picture"; + private static final String HEIGHT_PARAM = "height"; + private static final String WIDTH_PARAM = "width"; + private static final String MIGRATION_PARAM = "migration_overrides"; + private static final String MIGRATION_VALUE = "{october_2012:true}"; + + private Context context; + private URI imageUri; + private Callback callback; + private boolean allowCachedRedirects; + private Object callerTag; + + public static URI getProfilePictureUrl( + String userId, + int width, + int height) + throws URISyntaxException { + + Validate.notNullOrEmpty(userId, "userId"); + + width = Math.max(width, UNSPECIFIED_DIMENSION); + height = Math.max(height, UNSPECIFIED_DIMENSION); + + if (width == UNSPECIFIED_DIMENSION && height == UNSPECIFIED_DIMENSION) { + throw new IllegalArgumentException("Either width or height must be greater than 0"); + } + + Uri.Builder builder = new Uri.Builder().encodedPath(String.format(PROFILEPIC_URL_FORMAT, userId)); + + if (height != UNSPECIFIED_DIMENSION) { + builder.appendQueryParameter(HEIGHT_PARAM, String.valueOf(height)); + } + + if (width != UNSPECIFIED_DIMENSION) { + builder.appendQueryParameter(WIDTH_PARAM, String.valueOf(width)); + } + + builder.appendQueryParameter(MIGRATION_PARAM, MIGRATION_VALUE); + + return new URI(builder.toString()); + } + + private ImageRequest(Builder builder) { + this.context = builder.context; + this.imageUri = builder.imageUrl; + this.callback = builder.callback; + this.allowCachedRedirects = builder.allowCachedRedirects; + this.callerTag = builder.callerTag == null ? new Object() : builder.callerTag; + } + + public Context getContext() { + return context; + } + + public URI getImageUri() { + return imageUri; + } + + public Callback getCallback() { + return callback; + } + + public boolean isCachedRedirectAllowed() { + return allowCachedRedirects; + } + + public Object getCallerTag() { + return callerTag; + } + + public static class Builder { + // Required + private Context context; + private URI imageUrl; + + // Optional + private Callback callback; + private boolean allowCachedRedirects; + private Object callerTag; + + public Builder(Context context, URI imageUrl) { + Validate.notNull(imageUrl, "imageUrl"); + this.context = context; + this.imageUrl = imageUrl; + } + + public Builder setCallback(Callback callback) { + this.callback = callback; + return this; + } + + public Builder setCallerTag(Object callerTag) { + this.callerTag = callerTag; + return this; + } + + public Builder setAllowCachedRedirects(boolean allowCachedRedirects) { + this.allowCachedRedirects = allowCachedRedirects; + return this; + } + + public ImageRequest build() { + return new ImageRequest(this); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageResponse.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageResponse.java new file mode 100644 index 0000000000..ce2d6045b6 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageResponse.java @@ -0,0 +1,50 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.graphics.Bitmap; + +public class ImageResponse { + + private ImageRequest request; + private Exception error; + private boolean isCachedRedirect; + private Bitmap bitmap; + + ImageResponse(ImageRequest request, Exception error, boolean isCachedRedirect, Bitmap bitmap) { + this.request = request; + this.error = error; + this.bitmap = bitmap; + this.isCachedRedirect = isCachedRedirect; + } + + public ImageRequest getRequest() { + return request; + } + + public Exception getError() { + return error; + } + + public Bitmap getBitmap() { + return bitmap; + } + + public boolean isCachedRedirect() { + return isCachedRedirect; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageResponseCache.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageResponseCache.java new file mode 100644 index 0000000000..c61fd38ba3 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ImageResponseCache.java @@ -0,0 +1,122 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.util.Log; +import com.facebook.LoggingBehavior; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +class ImageResponseCache { + static final String TAG = ImageResponseCache.class.getSimpleName(); + + private volatile static FileLruCache imageCache; + + synchronized static FileLruCache getCache(Context context) throws IOException{ + if (imageCache == null) { + imageCache = new FileLruCache(context.getApplicationContext(), TAG, new FileLruCache.Limits()); + } + return imageCache; + } + + // Get stream from cache, or return null if the image is not cached. + // Does not throw if there was an error. + static InputStream getCachedImageStream(URI url, Context context) { + InputStream imageStream = null; + if (url != null) { + if (isCDNURL(url)) { + try { + FileLruCache cache = getCache(context); + imageStream = cache.get(url.toString()); + } catch (IOException e) { + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, e.toString()); + } + } + } + + return imageStream; + } + + static InputStream interceptAndCacheImageStream(Context context, HttpURLConnection connection) throws IOException { + InputStream stream = null; + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + URL url = connection.getURL(); + stream = connection.getInputStream(); // Default stream in case caching fails + try { + if (isCDNURL(url.toURI())) { + FileLruCache cache = getCache(context); + + // Wrap stream with a caching stream + stream = cache.interceptAndPut( + url.toString(), + new BufferedHttpInputStream(stream, connection)); + } + } catch (IOException e) { + // Caching is best effort + } catch (URISyntaxException e) { + // Caching is best effort + } + } + return stream; + } + + private static boolean isCDNURL(URI url) { + if (url != null) { + String uriHost = url.getHost(); + + if (uriHost.endsWith("fbcdn.net")) { + return true; + } + + if (uriHost.startsWith("fbcdn") && uriHost.endsWith("akamaihd.net")) { + return true; + } + } + + return false; + } + + static void clearCache(Context context) { + try { + getCache(context).clearCache(); + } catch (IOException e) { + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "clearCache failed " + e.getMessage()); + } + } + + private static class BufferedHttpInputStream extends BufferedInputStream { + HttpURLConnection connection; + BufferedHttpInputStream(InputStream stream, HttpURLConnection connection) { + super(stream, Utility.DEFAULT_STREAM_BUFFER_SIZE); + this.connection = connection; + } + + @Override + public void close() throws IOException { + super.close(); + Utility.disconnectQuietly(connection); + } + } +} + diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Logger.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Logger.java new file mode 100644 index 0000000000..a0d43fbbf2 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Logger.java @@ -0,0 +1,144 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.util.Log; +import com.facebook.LoggingBehavior; +import com.facebook.Settings; + +import java.util.HashMap; +import java.util.Map; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public class Logger { + public static final String LOG_TAG_BASE = "FacebookSDK."; + private static final HashMap stringsToReplace = new HashMap(); + + private final LoggingBehavior behavior; + private final String tag; + private StringBuilder contents; + private int priority = Log.DEBUG; + + // Note that the mapping of replaced strings is never emptied, so it should be used only for things that + // are not expected to be too numerous, such as access tokens. + public synchronized static void registerStringToReplace(String original, String replace) { + stringsToReplace.put(original, replace); + } + + public synchronized static void registerAccessToken(String accessToken) { + if (Settings.isLoggingBehaviorEnabled(LoggingBehavior.INCLUDE_ACCESS_TOKENS) == false) { + registerStringToReplace(accessToken, "ACCESS_TOKEN_REMOVED"); + } + } + + public static void log(LoggingBehavior behavior, String tag, String string) { + log(behavior, Log.DEBUG, tag, string); + } + + public static void log(LoggingBehavior behavior, String tag, String format, Object... args) { + if (Settings.isLoggingBehaviorEnabled(behavior)) { + String string = String.format(format, args); + log(behavior, Log.DEBUG, tag, string); + } + } + + public static void log(LoggingBehavior behavior, int priority, String tag, String string) { + if (Settings.isLoggingBehaviorEnabled(behavior)) { + string = replaceStrings(string); + if (tag.startsWith(LOG_TAG_BASE) == false) { + tag = LOG_TAG_BASE + tag; + } + Log.println(priority, tag, string); + + // Developer errors warrant special treatment by printing out a stack trace, to make both more noticeable, + // and let the source of the problem be more easily pinpointed. + if (behavior == LoggingBehavior.DEVELOPER_ERRORS) { + (new Exception()).printStackTrace(); + } + } + } + + private synchronized static String replaceStrings(String string) { + for (Map.Entry entry : stringsToReplace.entrySet()) { + string = string.replace(entry.getKey(), entry.getValue()); + } + return string; + } + + public Logger(LoggingBehavior behavior, String tag) { + Validate.notNullOrEmpty(tag, "tag"); + + this.behavior = behavior; + this.tag = LOG_TAG_BASE + tag; + this.contents = new StringBuilder(); + } + + public int getPriority() { + return priority; + } + + public void setPriority(int value) { + Validate.oneOf(value, "value", Log.ASSERT, Log.DEBUG, Log.ERROR, Log.INFO, Log.VERBOSE, Log.WARN); + + priority = value; + } + + public String getContents() { + return replaceStrings(contents.toString()); + } + + // Writes the accumulated contents, then clears contents to start again. + public void log() { + logString(contents.toString()); + contents = new StringBuilder(); + } + + // Immediately logs a string, ignoring any accumulated contents, which are left unchanged. + public void logString(String string) { + log(behavior, priority, tag, string); + } + + public void append(StringBuilder stringBuilder) { + if (shouldLog()) { + contents.append(stringBuilder); + } + } + + public void append(String string) { + if (shouldLog()) { + contents.append(string); + } + } + + public void append(String format, Object... args) { + if (shouldLog()) { + contents.append(String.format(format, args)); + } + } + + public void appendKeyValue(String key, Object value) { + append(" %s:\t%s\n", key, value); + } + + private boolean shouldLog() { + return Settings.isLoggingBehaviorEnabled(behavior); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/NativeProtocol.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/NativeProtocol.java new file mode 100644 index 0000000000..7a7186c10d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/NativeProtocol.java @@ -0,0 +1,504 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.*; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import com.facebook.*; + +import java.util.*; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public final class NativeProtocol { + + public static final int NO_PROTOCOL_AVAILABLE = -1; + + private static final String FACEBOOK_PROXY_AUTH_ACTIVITY = "com.facebook.katana.ProxyAuth"; + private static final String FACEBOOK_TOKEN_REFRESH_ACTIVITY = "com.facebook.katana.platform.TokenRefreshService"; + + public static final String FACEBOOK_PROXY_AUTH_PERMISSIONS_KEY = "scope"; + public static final String FACEBOOK_PROXY_AUTH_APP_ID_KEY = "client_id"; + public static final String FACEBOOK_PROXY_AUTH_E2E_KEY = "e2e"; + + // --------------------------------------------------------------------------------------------- + // Native Protocol updated 2012-11 + + static final String INTENT_ACTION_PLATFORM_ACTIVITY = "com.facebook.platform.PLATFORM_ACTIVITY"; + static final String INTENT_ACTION_PLATFORM_SERVICE = "com.facebook.platform.PLATFORM_SERVICE"; + + public static final int PROTOCOL_VERSION_20121101 = 20121101; + public static final int PROTOCOL_VERSION_20130502 = 20130502; + public static final int PROTOCOL_VERSION_20130618 = 20130618; + public static final int PROTOCOL_VERSION_20131107 = 20131107; + public static final int PROTOCOL_VERSION_20140204 = 20140204; + public static final int PROTOCOL_VERSION_20140324 = 20140324; + + public static final String EXTRA_PROTOCOL_VERSION = "com.facebook.platform.protocol.PROTOCOL_VERSION"; + public static final String EXTRA_PROTOCOL_ACTION = "com.facebook.platform.protocol.PROTOCOL_ACTION"; + public static final String EXTRA_PROTOCOL_CALL_ID = "com.facebook.platform.protocol.CALL_ID"; + public static final String EXTRA_GET_INSTALL_DATA_PACKAGE = "com.facebook.platform.extra.INSTALLDATA_PACKAGE"; + + // Messages supported by PlatformService: + public static final int MESSAGE_GET_ACCESS_TOKEN_REQUEST = 0x10000; + public static final int MESSAGE_GET_ACCESS_TOKEN_REPLY = 0x10001; + static final int MESSAGE_GET_PROTOCOL_VERSIONS_REQUEST = 0x10002; + static final int MESSAGE_GET_PROTOCOL_VERSIONS_REPLY = 0x10003; + public static final int MESSAGE_GET_INSTALL_DATA_REQUEST = 0x10004; + public static final int MESSAGE_GET_INSTALL_DATA_REPLY = 0x10005; + + // MESSAGE_ERROR_REPLY data keys: + // See STATUS_* + + // MESSAGE_GET_ACCESS_TOKEN_REQUEST data keys: + // EXTRA_APPLICATION_ID + + // MESSAGE_GET_ACCESS_TOKEN_REPLY data keys: + // EXTRA_ACCESS_TOKEN + // EXTRA_EXPIRES_SECONDS_SINCE_EPOCH + // EXTRA_PERMISSIONS + + // MESSAGE_GET_PROTOCOL_VERSIONS_REPLY data keys: + static final String EXTRA_PROTOCOL_VERSIONS = "com.facebook.platform.extra.PROTOCOL_VERSIONS"; + + // Values of EXTRA_PROTOCOL_ACTION supported by PlatformActivity: + public static final String ACTION_FEED_DIALOG = "com.facebook.platform.action.request.FEED_DIALOG"; + public static final String ACTION_MESSAGE_DIALOG = "com.facebook.platform.action.request.MESSAGE_DIALOG"; + public static final String ACTION_OGACTIONPUBLISH_DIALOG = + "com.facebook.platform.action.request.OGACTIONPUBLISH_DIALOG"; + public static final String ACTION_OGMESSAGEPUBLISH_DIALOG = + "com.facebook.platform.action.request.OGMESSAGEPUBLISH_DIALOG"; + + // Values of EXTRA_PROTOCOL_ACTION values returned by PlatformActivity: + public static final String ACTION_FEED_DIALOG_REPLY = + "com.facebook.platform.action.reply.FEED_DIALOG"; + public static final String ACTION_MESSAGE_DIALOG_REPLY = + "com.facebook.platform.action.reply.MESSAGE_DIALOG"; + public static final String ACTION_OGACTIONPUBLISH_DIALOG_REPLY = + "com.facebook.platform.action.reply.OGACTIONPUBLISH_DIALOG"; + public static final String ACTION_OGMESSAGEPUBLISH_DIALOG_REPLY = + "com.facebook.platform.action.reply.OGMESSAGEPUBLISH_DIALOG"; + + // Extras supported for ACTION_LOGIN_DIALOG: + public static final String EXTRA_PERMISSIONS = "com.facebook.platform.extra.PERMISSIONS"; + public static final String EXTRA_APPLICATION_ID = "com.facebook.platform.extra.APPLICATION_ID"; + public static final String EXTRA_APPLICATION_NAME = "com.facebook.platform.extra.APPLICATION_NAME"; + + // Extras returned by setResult() for ACTION_LOGIN_DIALOG + public static final String EXTRA_ACCESS_TOKEN = "com.facebook.platform.extra.ACCESS_TOKEN"; + public static final String EXTRA_EXPIRES_SECONDS_SINCE_EPOCH = + "com.facebook.platform.extra.EXPIRES_SECONDS_SINCE_EPOCH"; + // EXTRA_PERMISSIONS + + // Extras supported for ACTION_FEED_DIALOG: + public static final String EXTRA_PLACE_TAG = "com.facebook.platform.extra.PLACE"; + public static final String EXTRA_FRIEND_TAGS = "com.facebook.platform.extra.FRIENDS"; + public static final String EXTRA_LINK = "com.facebook.platform.extra.LINK"; + public static final String EXTRA_IMAGE = "com.facebook.platform.extra.IMAGE"; + public static final String EXTRA_TITLE = "com.facebook.platform.extra.TITLE"; + public static final String EXTRA_SUBTITLE = "com.facebook.platform.extra.SUBTITLE"; + public static final String EXTRA_DESCRIPTION = "com.facebook.platform.extra.DESCRIPTION"; + public static final String EXTRA_REF = "com.facebook.platform.extra.REF"; + public static final String EXTRA_DATA_FAILURES_FATAL = "com.facebook.platform.extra.DATA_FAILURES_FATAL"; + public static final String EXTRA_PHOTOS = "com.facebook.platform.extra.PHOTOS"; + + // Extras supported for ACTION_OGACTIONPUBLISH_DIALOG: + public static final String EXTRA_ACTION = "com.facebook.platform.extra.ACTION"; + public static final String EXTRA_ACTION_TYPE = "com.facebook.platform.extra.ACTION_TYPE"; + public static final String EXTRA_PREVIEW_PROPERTY_NAME = + "com.facebook.platform.extra.PREVIEW_PROPERTY_NAME"; + + // OG objects will have this key to set to true if they should be created as part of OG Action publish + public static final String OPEN_GRAPH_CREATE_OBJECT_KEY = "fbsdk:create_object"; + // Determines whether an image is user generated + public static final String IMAGE_USER_GENERATED_KEY = "user_generated"; + // url key for images + public static final String IMAGE_URL_KEY = "url"; + + // Keys for status data in MESSAGE_ERROR_REPLY from PlatformService and for error + // extras returned by PlatformActivity's setResult() in case of errors: + public static final String STATUS_ERROR_TYPE = "com.facebook.platform.status.ERROR_TYPE"; + public static final String STATUS_ERROR_DESCRIPTION = + "com.facebook.platform.status.ERROR_DESCRIPTION"; + public static final String STATUS_ERROR_CODE = "com.facebook.platform.status.ERROR_CODE"; + public static final String STATUS_ERROR_SUBCODE = "com.facebook.platform.status.ERROR_SUBCODE"; + public static final String STATUS_ERROR_JSON = "com.facebook.platform.status.ERROR_JSON"; + + // Expected values for ERROR_KEY_TYPE. Clients should tolerate other values: + public static final String ERROR_UNKNOWN_ERROR = "UnknownError"; + public static final String ERROR_PROTOCOL_ERROR = "ProtocolError"; + public static final String ERROR_USER_CANCELED = "UserCanceled"; + public static final String ERROR_APPLICATION_ERROR = "ApplicationError"; + public static final String ERROR_NETWORK_ERROR = "NetworkError"; + public static final String ERROR_PERMISSION_DENIED = "PermissionDenied"; + public static final String ERROR_SERVICE_DISABLED = "ServiceDisabled"; + + public static final String AUDIENCE_ME = "SELF"; + public static final String AUDIENCE_FRIENDS = "ALL_FRIENDS"; + public static final String AUDIENCE_EVERYONE = "EVERYONE"; + + // Request codes for different categories of native protocol calls. + public static final int DIALOG_REQUEST_CODE = 0xfacf; + + private static final String CONTENT_SCHEME = "content://"; + private static final String PLATFORM_PROVIDER_VERSIONS = ".provider.PlatformProvider/versions"; + + // Columns returned by PlatformProvider + private static final String PLATFORM_PROVIDER_VERSION_COLUMN = "version"; + + // Broadcast action for asynchronously-executing AppCalls + private static final String PLATFORM_ASYNC_APPCALL_ACTION = "com.facebook.platform.AppCallResultBroadcast"; + + private static abstract class NativeAppInfo { + abstract protected String getPackage(); + + private static final String FBI_HASH = "a4b7452e2ed8f5f191058ca7bbfd26b0d3214bfc"; + private static final String FBL_HASH = "5e8f16062ea3cd2c4a0d547876baa6f38cabf625"; + private static final String FBR_HASH = "8a3c4b262d721acd49a4bf97d5213199c86fa2b9"; + + private static final HashSet validAppSignatureHashes = buildAppSignatureHashes(); + + private static HashSet buildAppSignatureHashes() { + HashSet set = new HashSet(); + set.add(FBR_HASH); + set.add(FBI_HASH); + set.add(FBL_HASH); + return set; + } + + public boolean validateSignature(Context context, String packageName) { + String brand = Build.BRAND; + int applicationFlags = context.getApplicationInfo().flags; + if (brand.startsWith("generic") && (applicationFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + // We are debugging on an emulator, don't validate package signature. + return true; + } + + PackageInfo packageInfo = null; + try { + packageInfo = context.getPackageManager().getPackageInfo(packageName, + PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + + for (Signature signature : packageInfo.signatures) { + String hashedSignature = Utility.sha1hash(signature.toByteArray()); + if (validAppSignatureHashes.contains(hashedSignature)) { + return true; + } + } + + return false; + } + } + + private static class KatanaAppInfo extends NativeAppInfo { + static final String KATANA_PACKAGE = "com.facebook.katana"; + + @Override + protected String getPackage() { + return KATANA_PACKAGE; + } + } + + private static class MessengerAppInfo extends NativeAppInfo { + static final String MESSENGER_PACKAGE = "com.facebook.orca"; + + @Override + protected String getPackage() { + return MESSENGER_PACKAGE; + } + } + + private static class WakizashiAppInfo extends NativeAppInfo { + static final String WAKIZASHI_PACKAGE = "com.facebook.wakizashi"; + + @Override + protected String getPackage() { + return WAKIZASHI_PACKAGE; + } + } + + private static final NativeAppInfo FACEBOOK_APP_INFO = new KatanaAppInfo(); + private static List facebookAppInfoList = buildFacebookAppList(); + private static Map> actionToAppInfoMap = buildActionToAppInfoMap(); + + private static List buildFacebookAppList() { + List list = new ArrayList(); + + // Katana needs to be the first thing in the list since it will get selected as the default FACEBOOK_APP_INFO + list.add(FACEBOOK_APP_INFO); + list.add(new WakizashiAppInfo()); + + return list; + } + + private static Map> buildActionToAppInfoMap() { + Map> map = new HashMap>(); + + ArrayList messengerAppInfoList = new ArrayList(); + messengerAppInfoList.add(new MessengerAppInfo()); + + // Add individual actions and the list they should try + map.put(ACTION_OGACTIONPUBLISH_DIALOG, facebookAppInfoList); + map.put(ACTION_FEED_DIALOG, facebookAppInfoList); + map.put(ACTION_MESSAGE_DIALOG, messengerAppInfoList); + map.put(ACTION_OGMESSAGEPUBLISH_DIALOG, messengerAppInfoList); + + return map; + } + + static Intent validateActivityIntent(Context context, Intent intent, NativeAppInfo appInfo) { + if (intent == null) { + return null; + } + + ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent, 0); + if (resolveInfo == null) { + return null; + } + + if (!appInfo.validateSignature(context, resolveInfo.activityInfo.packageName)) { + return null; + } + + return intent; + } + + static Intent validateServiceIntent(Context context, Intent intent, NativeAppInfo appInfo) { + if (intent == null) { + return null; + } + + ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent, 0); + if (resolveInfo == null) { + return null; + } + + if (!appInfo.validateSignature(context, resolveInfo.serviceInfo.packageName)) { + return null; + } + + return intent; + } + + public static Intent createProxyAuthIntent(Context context, String applicationId, List permissions, + String e2e, boolean isRerequest) { + for (NativeAppInfo appInfo : facebookAppInfoList) { + Intent intent = new Intent() + .setClassName(appInfo.getPackage(), FACEBOOK_PROXY_AUTH_ACTIVITY) + .putExtra(FACEBOOK_PROXY_AUTH_APP_ID_KEY, applicationId); + + if (!Utility.isNullOrEmpty(permissions)) { + intent.putExtra(FACEBOOK_PROXY_AUTH_PERMISSIONS_KEY, TextUtils.join(",", permissions)); + } + if (!Utility.isNullOrEmpty(e2e)) { + intent.putExtra(FACEBOOK_PROXY_AUTH_E2E_KEY, e2e); + } + + intent.putExtra(ServerProtocol.DIALOG_PARAM_RESPONSE_TYPE, ServerProtocol.DIALOG_RESPONSE_TYPE_TOKEN); + intent.putExtra(ServerProtocol.DIALOG_PARAM_RETURN_SCOPES, ServerProtocol.DIALOG_RETURN_SCOPES_TRUE); + + if (!Settings.getPlatformCompatibilityEnabled()) { + // Override the API Version for Auth + intent.putExtra(ServerProtocol.DIALOG_PARAM_LEGACY_OVERRIDE, ServerProtocol.GRAPH_API_VERSION); + + // Only set the rerequest auth type for non legacy requests + if (isRerequest) { + intent.putExtra(ServerProtocol.DIALOG_PARAM_AUTH_TYPE, ServerProtocol.DIALOG_REREQUEST_AUTH_TYPE); + } + } + + intent = validateActivityIntent(context, intent, appInfo); + + if (intent != null) { + return intent; + } + } + return null; + } + + public static Intent createTokenRefreshIntent(Context context) { + for (NativeAppInfo appInfo : facebookAppInfoList) { + Intent intent = new Intent() + .setClassName(appInfo.getPackage(), FACEBOOK_TOKEN_REFRESH_ACTIVITY); + + intent = validateServiceIntent(context, intent, appInfo); + + if (intent != null) { + return intent; + } + } + return null; + } + + // Note: be sure this stays sorted in descending order; add new versions at the beginning + private static final List KNOWN_PROTOCOL_VERSIONS = + Arrays.asList( + PROTOCOL_VERSION_20140324, + PROTOCOL_VERSION_20140204, + PROTOCOL_VERSION_20131107, + PROTOCOL_VERSION_20130618, + PROTOCOL_VERSION_20130502, + PROTOCOL_VERSION_20121101 + ); + + private static Intent findActivityIntent(Context context, String activityAction, String internalAction) { + List list = actionToAppInfoMap.get(internalAction); + if (list == null) { + return null; + } + + Intent intent = null; + for (NativeAppInfo appInfo : list) { + intent = new Intent() + .setAction(activityAction) + .setPackage(appInfo.getPackage()) + .addCategory(Intent.CATEGORY_DEFAULT); + intent = validateActivityIntent(context, intent, appInfo); + if (intent != null) { + return intent; + } + } + + return intent; + } + + public static Intent createPlatformActivityIntent(Context context, String action, int version, Bundle extras) { + Intent intent = findActivityIntent(context, INTENT_ACTION_PLATFORM_ACTIVITY, action); + if (intent == null) { + return null; + } + + intent.putExtras(extras) + .putExtra(EXTRA_PROTOCOL_VERSION, version) + .putExtra(EXTRA_PROTOCOL_ACTION, action); + + return intent; + } + + public static Intent createPlatformServiceIntent(Context context) { + for (NativeAppInfo appInfo : facebookAppInfoList) { + Intent intent = new Intent(INTENT_ACTION_PLATFORM_SERVICE) + .setPackage(appInfo.getPackage()) + .addCategory(Intent.CATEGORY_DEFAULT); + intent = validateServiceIntent(context, intent, appInfo); + if (intent != null) { + return intent; + } + } + return null; + } + + public static boolean isErrorResult(Intent resultIntent) { + return resultIntent.hasExtra(STATUS_ERROR_TYPE); + } + + public static Exception getErrorFromResult(Intent resultIntent) { + if (!isErrorResult(resultIntent)) { + return null; + } + + String type = resultIntent.getStringExtra(STATUS_ERROR_TYPE); + String description = resultIntent.getStringExtra(STATUS_ERROR_DESCRIPTION); + + if (type.equalsIgnoreCase(ERROR_USER_CANCELED)) { + return new FacebookOperationCanceledException(description); + } + /* TODO parse error values and create appropriate exception class */ + return new FacebookException(description); + } + + public static int getLatestAvailableProtocolVersionForService(Context context, final int minimumVersion) { + // Services are currently always against the Facebook App + return getLatestAvailableProtocolVersionForAppInfoList(context, facebookAppInfoList, minimumVersion); + } + + public static int getLatestAvailableProtocolVersionForAction(Context context, String action, final int minimumVersion) { + List appInfoList = actionToAppInfoMap.get(action); + return getLatestAvailableProtocolVersionForAppInfoList(context, appInfoList, minimumVersion); + } + + private static int getLatestAvailableProtocolVersionForAppInfoList(Context context, List appInfoList, + final int minimumVersion) { + if (appInfoList == null) { + return NO_PROTOCOL_AVAILABLE; + } + + // Could potentially cache the NativeAppInfo to latestProtocolVersion + for (NativeAppInfo appInfo : appInfoList) { + int protocolVersion = getLatestAvailableProtocolVersionForAppInfo(context, appInfo, minimumVersion); + if (protocolVersion != NO_PROTOCOL_AVAILABLE) { + return protocolVersion; + } + } + + return NO_PROTOCOL_AVAILABLE; + } + + private static int getLatestAvailableProtocolVersionForAppInfo(Context context, NativeAppInfo appInfo, + final int minimumVersion) { + ContentResolver contentResolver = context.getContentResolver(); + + String [] projection = new String[]{ PLATFORM_PROVIDER_VERSION_COLUMN }; + Uri uri = buildPlatformProviderVersionURI(appInfo); + Cursor c = null; + try { + c = contentResolver.query(uri, projection, null, null, null); + if (c == null) { + return NO_PROTOCOL_AVAILABLE; + } + + Set versions = new HashSet(); + while (c.moveToNext()) { + int version = c.getInt(c.getColumnIndex(PLATFORM_PROVIDER_VERSION_COLUMN)); + versions.add(version); + } + + for (Integer knownVersion : KNOWN_PROTOCOL_VERSIONS) { + if (knownVersion < minimumVersion) { + return NO_PROTOCOL_AVAILABLE; + } + + if (versions.contains(knownVersion)) { + return knownVersion; + } + } + } finally { + if (c != null) { + c.close(); + } + } + + return NO_PROTOCOL_AVAILABLE; + } + + private static Uri buildPlatformProviderVersionURI(NativeAppInfo appInfo) { + return Uri.parse(CONTENT_SCHEME + appInfo.getPackage() + PLATFORM_PROVIDER_VERSIONS); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/PlatformServiceClient.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/PlatformServiceClient.java new file mode 100644 index 0000000000..16d619c3b7 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/PlatformServiceClient.java @@ -0,0 +1,151 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.*; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +abstract public class PlatformServiceClient implements ServiceConnection { + private final Context context; + private final Handler handler; + private CompletedListener listener; + private boolean running; + private Messenger sender; + private int requestMessage; + private int replyMessage; + private final String applicationId; + private final int protocolVersion; + + public PlatformServiceClient(Context context, int requestMessage, int replyMessage, int protocolVersion, + String applicationId) { + Context applicationContext = context.getApplicationContext(); + + this.context = (applicationContext != null) ? applicationContext : context; + this.requestMessage = requestMessage; + this.replyMessage = replyMessage; + this.applicationId = applicationId; + this.protocolVersion = protocolVersion; + + handler = new Handler() { + @Override + public void handleMessage(Message message) { + PlatformServiceClient.this.handleMessage(message); + } + }; + } + + public void setCompletedListener(CompletedListener listener) { + this.listener = listener; + } + + protected Context getContext() { + return context; + } + + public boolean start() { + if (running) { + return false; + } + + // Make sure that the service can handle the requested protocol version + int availableVersion = NativeProtocol.getLatestAvailableProtocolVersionForService(context, protocolVersion); + if (availableVersion == NativeProtocol.NO_PROTOCOL_AVAILABLE) { + return false; + } + + Intent intent = NativeProtocol.createPlatformServiceIntent(context); + if (intent == null) { + return false; + } else { + running = true; + context.bindService(intent, this, Context.BIND_AUTO_CREATE); + return true; + } + } + + public void cancel() { + running = false; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + sender = new Messenger(service); + sendMessage(); + } + + public void onServiceDisconnected(ComponentName name) { + sender = null; + context.unbindService(this); + callback(null); + } + + private void sendMessage() { + Bundle data = new Bundle(); + data.putString(NativeProtocol.EXTRA_APPLICATION_ID, applicationId); + + populateRequestBundle(data); + + Message request = Message.obtain(null, requestMessage); + request.arg1 = protocolVersion; + request.setData(data); + request.replyTo = new Messenger(handler); + + try { + sender.send(request); + } catch (RemoteException e) { + callback(null); + } + } + + protected abstract void populateRequestBundle(Bundle data); + + protected void handleMessage(Message message) { + if (message.what == replyMessage) { + Bundle extras = message.getData(); + String errorType = extras.getString(NativeProtocol.STATUS_ERROR_TYPE); + if (errorType != null) { + callback(null); + } else { + callback(extras); + } + context.unbindService(this); + } + } + + private void callback(Bundle result) { + if (!running) { + return; + } + running = false; + + CompletedListener callback = listener; + if (callback != null) { + callback.completed(result); + } + } + + public interface CompletedListener { + void completed(Bundle result); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ServerProtocol.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ServerProtocol.java new file mode 100644 index 0000000000..06155ce5ee --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/ServerProtocol.java @@ -0,0 +1,82 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import com.facebook.Settings; + +import java.util.Collection; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public final class ServerProtocol { + private static final String DIALOG_AUTHORITY_FORMAT = "m.%s"; + public static final String DIALOG_PATH = "dialog/"; + public static final String DIALOG_PARAM_ACCESS_TOKEN = "access_token"; + public static final String DIALOG_PARAM_APP_ID = "app_id"; + public static final String DIALOG_PARAM_AUTH_TYPE = "auth_type"; + public static final String DIALOG_PARAM_CLIENT_ID = "client_id"; + public static final String DIALOG_PARAM_DISPLAY = "display"; + public static final String DIALOG_PARAM_E2E = "e2e"; + public static final String DIALOG_PARAM_LEGACY_OVERRIDE = "legacy_override"; + public static final String DIALOG_PARAM_REDIRECT_URI = "redirect_uri"; + public static final String DIALOG_PARAM_RESPONSE_TYPE = "response_type"; + public static final String DIALOG_PARAM_RETURN_SCOPES = "return_scopes"; + public static final String DIALOG_PARAM_SCOPE = "scope"; + public static final String DIALOG_REREQUEST_AUTH_TYPE = "rerequest"; + public static final String DIALOG_RESPONSE_TYPE_TOKEN = "token"; + public static final String DIALOG_RETURN_SCOPES_TRUE = "true"; + + // URL components + private static final String GRAPH_VIDEO_URL_FORMAT = "https://graph-video.%s"; + private static final String GRAPH_URL_FORMAT = "https://graph.%s"; + private static final String REST_URL_FORMAT = "https://api.%s"; + public static final String REST_METHOD_BASE = "method"; + public static final String GRAPH_API_VERSION = "v2.0"; + + private static final String LEGACY_API_VERSION = "v1.0"; + + public static final Collection errorsProxyAuthDisabled = + Utility.unmodifiableCollection("service_disabled", "AndroidAuthKillSwitchException"); + public static final Collection errorsUserCanceled = + Utility.unmodifiableCollection("access_denied", "OAuthAccessDeniedException"); + + public static final String getDialogAuthority() { + return String.format(DIALOG_AUTHORITY_FORMAT, Settings.getFacebookDomain()); + } + + public static final String getGraphUrlBase() { + return String.format(GRAPH_URL_FORMAT, Settings.getFacebookDomain()); + } + + public static final String getGraphVideoUrlBase() { + return String.format(GRAPH_VIDEO_URL_FORMAT, Settings.getFacebookDomain()); + } + + public static final String getRestUrlBase() { + return String.format(REST_URL_FORMAT, Settings.getFacebookDomain()); + } + + public static final String getAPIVersion() { + if (Settings.getPlatformCompatibilityEnabled()) { + return LEGACY_API_VERSION; + } + return GRAPH_API_VERSION; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/SessionAuthorizationType.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/SessionAuthorizationType.java new file mode 100644 index 0000000000..85083b8d6a --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/SessionAuthorizationType.java @@ -0,0 +1,27 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public enum SessionAuthorizationType { + READ, + PUBLISH +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/SessionTracker.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/SessionTracker.java new file mode 100644 index 0000000000..5584ef3be6 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/SessionTracker.java @@ -0,0 +1,239 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.v4.content.LocalBroadcastManager; +import com.facebook.Session; +import com.facebook.SessionState; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public class SessionTracker { + + private Session session; + private final Session.StatusCallback callback; + private final BroadcastReceiver receiver; + private final LocalBroadcastManager broadcastManager; + private boolean isTracking = false; + + /** + * Constructs a SessionTracker to track the active Session object. + * + * @param context the context object. + * @param callback the callback to use whenever the active Session's + * state changes + */ + public SessionTracker(Context context, Session.StatusCallback callback) { + this(context, callback, null); + } + + /** + * Constructs a SessionTracker to track the Session object passed in. + * If the Session is null, then it will track the active Session instead. + * + * @param context the context object. + * @param callback the callback to use whenever the Session's state changes + * @param session the Session object to track + */ + SessionTracker(Context context, Session.StatusCallback callback, Session session) { + this(context, callback, session, true); + } + + /** + * Constructs a SessionTracker to track the Session object passed in. + * If the Session is null, then it will track the active Session instead. + * + * @param context the context object. + * @param callback the callback to use whenever the Session's state changes + * @param session the Session object to track + * @param startTracking whether to start tracking the Session right away + */ + public SessionTracker(Context context, Session.StatusCallback callback, Session session, boolean startTracking) { + this.callback = new CallbackWrapper(callback); + this.session = session; + this.receiver = new ActiveSessionBroadcastReceiver(); + this.broadcastManager = LocalBroadcastManager.getInstance(context); + + if (startTracking) { + startTracking(); + } + } + + /** + * Returns the current Session that's being tracked. + * + * @return the current Session associated with this tracker + */ + public Session getSession() { + return (session == null) ? Session.getActiveSession() : session; + } + + /** + * Returns the current Session that's being tracked if it's open, + * otherwise returns null. + * + * @return the current Session if it's open, otherwise returns null + */ + public Session getOpenSession() { + Session openSession = getSession(); + if (openSession != null && openSession.isOpened()) { + return openSession; + } + return null; + } + + /** + * Set the Session object to track. + * + * @param newSession the new Session object to track + */ + public void setSession(Session newSession) { + if (newSession == null) { + if (session != null) { + // We're current tracking a Session. Remove the callback + // and start tracking the active Session. + session.removeCallback(callback); + session = null; + addBroadcastReceiver(); + if (getSession() != null) { + getSession().addCallback(callback); + } + } + } else { + if (session == null) { + // We're currently tracking the active Session, but will be + // switching to tracking a different Session object. + Session activeSession = Session.getActiveSession(); + if (activeSession != null) { + activeSession.removeCallback(callback); + } + broadcastManager.unregisterReceiver(receiver); + } else { + // We're currently tracking a Session, but are now switching + // to a new Session, so we remove the callback from the old + // Session, and add it to the new one. + session.removeCallback(callback); + } + session = newSession; + session.addCallback(callback); + } + } + + /** + * Start tracking the Session (either active or the one given). + */ + public void startTracking() { + if (isTracking) { + return; + } + if (this.session == null) { + addBroadcastReceiver(); + } + // if the session is not null, then add the callback to it right away + if (getSession() != null) { + getSession().addCallback(callback); + } + isTracking = true; + } + + /** + * Stop tracking the Session and remove any callbacks attached + * to those sessions. + */ + public void stopTracking() { + if (!isTracking) { + return; + } + Session session = getSession(); + if (session != null) { + session.removeCallback(callback); + } + broadcastManager.unregisterReceiver(receiver); + isTracking = false; + } + + /** + * Returns whether it's currently tracking the Session. + * + * @return true if currently tracking the Session + */ + public boolean isTracking() { + return isTracking; + } + + /** + * Returns whether it's currently tracking the active Session. + * + * @return true if the currently tracked session is the active Session. + */ + public boolean isTrackingActiveSession() { + return session == null; + } + + private void addBroadcastReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Session.ACTION_ACTIVE_SESSION_SET); + filter.addAction(Session.ACTION_ACTIVE_SESSION_UNSET); + + // Add a broadcast receiver to listen to when the active Session + // is set or unset, and add/remove our callback as appropriate + broadcastManager.registerReceiver(receiver, filter); + } + + /** + * The BroadcastReceiver implementation that either adds or removes the callback + * from the active Session object as it's SET or UNSET. + */ + private class ActiveSessionBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Session.ACTION_ACTIVE_SESSION_SET.equals(intent.getAction())) { + Session session = Session.getActiveSession(); + if (session != null) { + session.addCallback(SessionTracker.this.callback); + } + } + } + } + + private class CallbackWrapper implements Session.StatusCallback { + + private final Session.StatusCallback wrapped; + public CallbackWrapper(Session.StatusCallback wrapped) { + this.wrapped = wrapped; + } + + @Override + public void call(Session session, SessionState state, Exception exception) { + if (wrapped != null && isTracking()) { + wrapped.call(session, state, exception); + } + // if we're not tracking the Active Session, and the current session + // is closed, then start tracking the Active Session. + if (session == SessionTracker.this.session && state.isClosed()) { + setSession(null); + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/UrlRedirectCache.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/UrlRedirectCache.java new file mode 100644 index 0000000000..2123da8ff4 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/UrlRedirectCache.java @@ -0,0 +1,108 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.util.Log; +import com.facebook.LoggingBehavior; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; + +class UrlRedirectCache { + static final String TAG = UrlRedirectCache.class.getSimpleName(); + private static final String REDIRECT_CONTENT_TAG = TAG + "_Redirect"; + + private volatile static FileLruCache urlRedirectCache; + + synchronized static FileLruCache getCache(Context context) throws IOException{ + if (urlRedirectCache == null) { + urlRedirectCache = new FileLruCache(context.getApplicationContext(), TAG, new FileLruCache.Limits()); + } + return urlRedirectCache; + } + + static URI getRedirectedUri(Context context, URI uri) { + if (uri == null) { + return null; + } + + String uriString = uri.toString(); + InputStreamReader reader = null; + try { + InputStream stream; + FileLruCache cache = getCache(context); + boolean redirectExists = false; + while ((stream = cache.get(uriString, REDIRECT_CONTENT_TAG)) != null) { + redirectExists = true; + + // Get the redirected url + reader = new InputStreamReader(stream); + char[] buffer = new char[128]; + int bufferLength; + StringBuilder urlBuilder = new StringBuilder(); + while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) { + urlBuilder.append(buffer, 0, bufferLength); + } + Utility.closeQuietly(reader); + + // Iterate to the next url in the redirection + uriString = urlBuilder.toString(); + } + + if (redirectExists) { + return new URI(uriString); + } + } catch (URISyntaxException e) { + // caching is best effort, so ignore the exception + } catch (IOException ioe) { + } finally { + Utility.closeQuietly(reader); + } + + return null; + } + + static void cacheUriRedirect(Context context, URI fromUri, URI toUri) { + if (fromUri == null || toUri == null) { + return; + } + + OutputStream redirectStream = null; + try { + FileLruCache cache = getCache(context); + redirectStream = cache.openPutStream(fromUri.toString(), REDIRECT_CONTENT_TAG); + redirectStream.write(toUri.toString().getBytes()); + } catch (IOException e) { + // Caching is best effort + } finally { + Utility.closeQuietly(redirectStream); + } + } + + static void clearCache(Context context) { + try { + getCache(context).clearCache(); + } catch (IOException e) { + Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "clearCache failed " + e.getMessage()); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Utility.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Utility.java new file mode 100644 index 0000000000..7289c1477a --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Utility.java @@ -0,0 +1,501 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.provider.Settings.Secure; +import android.text.TextUtils; +import android.util.Log; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import com.facebook.*; +import com.facebook.android.BuildConfig; +import com.facebook.model.GraphObject; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.URLConnection; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public final class Utility { + static final String LOG_TAG = "FacebookSDK"; + private static final String HASH_ALGORITHM_MD5 = "MD5"; + private static final String HASH_ALGORITHM_SHA1 = "SHA-1"; + private static final String URL_SCHEME = "https"; + private static final String SUPPORTS_ATTRIBUTION = "supports_attribution"; + private static final String SUPPORTS_IMPLICIT_SDK_LOGGING = "supports_implicit_sdk_logging"; + private static final String NUX_CONTENT = "gdpv4_nux_content"; + private static final String NUX_ENABLED = "gdpv4_nux_enabled"; + + private static final String [] APP_SETTING_FIELDS = new String[] { + SUPPORTS_ATTRIBUTION, + SUPPORTS_IMPLICIT_SDK_LOGGING, + NUX_CONTENT, + NUX_ENABLED + }; + private static final String APPLICATION_FIELDS = "fields"; + + // This is the default used by the buffer streams, but they trace a warning if you do not specify. + public static final int DEFAULT_STREAM_BUFFER_SIZE = 8192; + + private static Map fetchedAppSettings = + new ConcurrentHashMap(); + + public static class FetchedAppSettings { + private boolean supportsAttribution; + private boolean supportsImplicitLogging; + private String nuxContent; + private boolean nuxEnabled; + + private FetchedAppSettings(boolean supportsAttribution, + boolean supportsImplicitLogging, + String nuxContent, + boolean nuxEnabled) { + this.supportsAttribution = supportsAttribution; + this.supportsImplicitLogging = supportsImplicitLogging; + this.nuxContent = nuxContent; + this.nuxEnabled = nuxEnabled; + } + + public boolean supportsAttribution() { + return supportsAttribution; + } + + public boolean supportsImplicitLogging() { + return supportsImplicitLogging; + } + + public String getNuxContent() { + return nuxContent; + } + + public boolean getNuxEnabled() { + return nuxEnabled; + } + } + + // Returns true iff all items in subset are in superset, treating null and + // empty collections as + // the same. + public static boolean isSubset(Collection subset, Collection superset) { + if ((superset == null) || (superset.size() == 0)) { + return ((subset == null) || (subset.size() == 0)); + } + + HashSet hash = new HashSet(superset); + for (T t : subset) { + if (!hash.contains(t)) { + return false; + } + } + return true; + } + + public static boolean isNullOrEmpty(Collection c) { + return (c == null) || (c.size() == 0); + } + + public static boolean isNullOrEmpty(String s) { + return (s == null) || (s.length() == 0); + } + + public static Collection unmodifiableCollection(T... ts) { + return Collections.unmodifiableCollection(Arrays.asList(ts)); + } + + public static ArrayList arrayList(T... ts) { + ArrayList arrayList = new ArrayList(ts.length); + for (T t : ts) { + arrayList.add(t); + } + return arrayList; + } + + static String md5hash(String key) { + return hashWithAlgorithm(HASH_ALGORITHM_MD5, key); + } + + static String sha1hash(String key) { + return hashWithAlgorithm(HASH_ALGORITHM_SHA1, key); + } + + static String sha1hash(byte[] bytes) { + return hashWithAlgorithm(HASH_ALGORITHM_SHA1, bytes); + } + + private static String hashWithAlgorithm(String algorithm, String key) { + return hashWithAlgorithm(algorithm, key.getBytes()); + } + + private static String hashWithAlgorithm(String algorithm, byte[] bytes) { + MessageDigest hash; + try { + hash = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + return null; + } + return hashBytes(hash, bytes); + } + + private static String hashBytes(MessageDigest hash, byte[] bytes) { + hash.update(bytes); + byte[] digest = hash.digest(); + StringBuilder builder = new StringBuilder(); + for (int b : digest) { + builder.append(Integer.toHexString((b >> 4) & 0xf)); + builder.append(Integer.toHexString((b >> 0) & 0xf)); + } + return builder.toString(); + } + + public static Uri buildUri(String authority, String path, Bundle parameters) { + Uri.Builder builder = new Uri.Builder(); + builder.scheme(URL_SCHEME); + builder.authority(authority); + builder.path(path); + for (String key : parameters.keySet()) { + Object parameter = parameters.get(key); + if (parameter instanceof String) { + builder.appendQueryParameter(key, (String) parameter); + } + } + return builder.build(); + } + + public static void putObjectInBundle(Bundle bundle, String key, Object value) { + if (value instanceof String) { + bundle.putString(key, (String) value); + } else if (value instanceof Parcelable) { + bundle.putParcelable(key, (Parcelable) value); + } else if (value instanceof byte[]) { + bundle.putByteArray(key, (byte[]) value); + } else { + throw new FacebookException("attempted to add unsupported type to Bundle"); + } + } + + public static void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + public static void disconnectQuietly(URLConnection connection) { + if (connection instanceof HttpURLConnection) { + ((HttpURLConnection)connection).disconnect(); + } + } + + public static String getMetadataApplicationId(Context context) { + Validate.notNull(context, "context"); + + Settings.loadDefaultsFromMetadata(context); + + return Settings.getApplicationId(); + } + + static Map convertJSONObjectToHashMap(JSONObject jsonObject) { + HashMap map = new HashMap(); + JSONArray keys = jsonObject.names(); + for (int i = 0; i < keys.length(); ++i) { + String key; + try { + key = keys.getString(i); + Object value = jsonObject.get(key); + if (value instanceof JSONObject) { + value = convertJSONObjectToHashMap((JSONObject) value); + } + map.put(key, value); + } catch (JSONException e) { + } + } + return map; + } + + // Returns either a JSONObject or JSONArray representation of the 'key' property of 'jsonObject'. + public static Object getStringPropertyAsJSON(JSONObject jsonObject, String key, String nonJSONPropertyKey) + throws JSONException { + Object value = jsonObject.opt(key); + if (value != null && value instanceof String) { + JSONTokener tokener = new JSONTokener((String) value); + value = tokener.nextValue(); + } + + if (value != null && !(value instanceof JSONObject || value instanceof JSONArray)) { + if (nonJSONPropertyKey != null) { + // Facebook sometimes gives us back a non-JSON value such as + // literal "true" or "false" as a result. + // If we got something like that, we present it to the caller as + // a GraphObject with a single + // property. We only do this if the caller wants that behavior. + jsonObject = new JSONObject(); + jsonObject.putOpt(nonJSONPropertyKey, value); + return jsonObject; + } else { + throw new FacebookException("Got an unexpected non-JSON object."); + } + } + + return value; + + } + + public static String readStreamToString(InputStream inputStream) throws IOException { + BufferedInputStream bufferedInputStream = null; + InputStreamReader reader = null; + try { + bufferedInputStream = new BufferedInputStream(inputStream); + reader = new InputStreamReader(bufferedInputStream); + StringBuilder stringBuilder = new StringBuilder(); + + final int bufferSize = 1024 * 2; + char[] buffer = new char[bufferSize]; + int n = 0; + while ((n = reader.read(buffer)) != -1) { + stringBuilder.append(buffer, 0, n); + } + + return stringBuilder.toString(); + } finally { + closeQuietly(bufferedInputStream); + closeQuietly(reader); + } + } + + public static boolean stringsEqualOrEmpty(String a, String b) { + boolean aEmpty = TextUtils.isEmpty(a); + boolean bEmpty = TextUtils.isEmpty(b); + + if (aEmpty && bEmpty) { + // Both null or empty, they match. + return true; + } + if (!aEmpty && !bEmpty) { + // Both non-empty, check equality. + return a.equals(b); + } + // One empty, one non-empty, can't match. + return false; + } + + private static void clearCookiesForDomain(Context context, String domain) { + // This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager + // has never been created. + CookieSyncManager syncManager = CookieSyncManager.createInstance(context); + syncManager.sync(); + + CookieManager cookieManager = CookieManager.getInstance(); + + String cookies = cookieManager.getCookie(domain); + if (cookies == null) { + return; + } + + String[] splitCookies = cookies.split(";"); + for (String cookie : splitCookies) { + String[] cookieParts = cookie.split("="); + if (cookieParts.length > 0) { + String newCookie = cookieParts[0].trim() + "=;expires=Sat, 1 Jan 2000 00:00:01 UTC;"; + cookieManager.setCookie(domain, newCookie); + } + } + cookieManager.removeExpiredCookie(); + } + + public static void clearFacebookCookies(Context context) { + // setCookie acts differently when trying to expire cookies between builds of Android that are using + // Chromium HTTP stack and those that are not. Using both of these domains to ensure it works on both. + clearCookiesForDomain(context, "facebook.com"); + clearCookiesForDomain(context, ".facebook.com"); + clearCookiesForDomain(context, "https://facebook.com"); + clearCookiesForDomain(context, "https://.facebook.com"); + } + + public static void logd(String tag, Exception e) { + if (BuildConfig.DEBUG && tag != null && e != null) { + Log.d(tag, e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + public static void logd(String tag, String msg) { + if (BuildConfig.DEBUG && tag != null && msg != null) { + Log.d(tag, msg); + } + } + + public static boolean areObjectsEqual(T a, T b) { + if (a == null) { + return b == null; + } + return a.equals(b); + } + + // Note that this method makes a synchronous Graph API call, so should not be called from the main thread. + public static FetchedAppSettings queryAppSettings(final String applicationId, final boolean forceRequery) { + + // Cache the last app checked results. + if (!forceRequery && fetchedAppSettings.containsKey(applicationId)) { + return fetchedAppSettings.get(applicationId); + } + + Bundle appSettingsParams = new Bundle(); + appSettingsParams.putString(APPLICATION_FIELDS, TextUtils.join(",", APP_SETTING_FIELDS)); + + Request request = Request.newGraphPathRequest(null, applicationId, null); + request.setParameters(appSettingsParams); + + GraphObject supportResponse = request.executeAndWait().getGraphObject(); + FetchedAppSettings result = new FetchedAppSettings( + safeGetBooleanFromResponse(supportResponse, SUPPORTS_ATTRIBUTION), + safeGetBooleanFromResponse(supportResponse, SUPPORTS_IMPLICIT_SDK_LOGGING), + safeGetStringFromResponse(supportResponse, NUX_CONTENT), + safeGetBooleanFromResponse(supportResponse, NUX_ENABLED) + ); + + fetchedAppSettings.put(applicationId, result); + + return result; + } + + private static boolean safeGetBooleanFromResponse(GraphObject response, String propertyName) { + Object result = false; + if (response != null) { + result = response.getProperty(propertyName); + } + if (!(result instanceof Boolean)) { + result = false; + } + return (Boolean) result; + } + + private static String safeGetStringFromResponse(GraphObject response, String propertyName) { + Object result = ""; + if (response != null) { + result = response.getProperty(propertyName); + } + if (!(result instanceof String)) { + result = ""; + } + return (String) result; + } + + public static void clearCaches(Context context) { + ImageDownloader.clearCache(context); + } + + public static void deleteDirectory(File directoryOrFile) { + if (!directoryOrFile.exists()) { + return; + } + + if (directoryOrFile.isDirectory()) { + for (File child : directoryOrFile.listFiles()) { + deleteDirectory(child); + } + } + directoryOrFile.delete(); + } + + public static List asListNoNulls(T... array) { + ArrayList result = new ArrayList(); + for (T t : array) { + if (t != null) { + result.add(t); + } + } + return result; + } + + // Return a hash of the android_id combined with the appid. Intended to dedupe requests on the server side + // in order to do counting of users unknown to Facebook. Because we put the appid into the key prior to hashing, + // we cannot do correlation of the same user across multiple apps -- this is intentional. When we transition to + // the Google advertising ID, we'll get rid of this and always send that up. + public static String getHashedDeviceAndAppID(Context context, String applicationId) { + String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); + + if (androidId == null) { + return null; + } else { + return sha1hash(androidId + applicationId); + } + } + + public static void setAppEventAttributionParameters(GraphObject params, + AttributionIdentifiers attributionIdentifiers, String hashedDeviceAndAppId, boolean limitEventUsage) { + // Send attributionID if it exists, otherwise send a hashed device+appid specific value as the advertiser_id. + if (attributionIdentifiers != null && attributionIdentifiers.getAttributionId() != null) { + params.setProperty("attribution", attributionIdentifiers.getAttributionId()); + } + + if (attributionIdentifiers != null && attributionIdentifiers.getAndroidAdvertiserId() != null) { + params.setProperty("advertiser_id", attributionIdentifiers.getAndroidAdvertiserId()); + params.setProperty("advertiser_tracking_enabled", !attributionIdentifiers.isTrackingLimited()); + } else if (hashedDeviceAndAppId != null) { + params.setProperty("advertiser_id", hashedDeviceAndAppId); + } + + params.setProperty("application_tracking_enabled", !limitEventUsage); + } + + public static Method getMethodQuietly(Class clazz, String methodName, Class... parameterTypes) { + try { + return clazz.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException ex) { + return null; + } + } + + public static Method getMethodQuietly(String className, String methodName, Class... parameterTypes) { + try { + Class clazz = Class.forName(className); + return getMethodQuietly(clazz, methodName, parameterTypes); + } catch (ClassNotFoundException ex) { + return null; + } + } + + public static Object invokeMethodQuietly(Object receiver, Method method, Object... args) { + try { + return method.invoke(receiver, args); + } catch (IllegalAccessException ex) { + return null; + } catch (InvocationTargetException ex) { + return null; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Validate.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Validate.java new file mode 100644 index 0000000000..5f74ac97c9 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/Validate.java @@ -0,0 +1,85 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import java.util.Collection; + +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +public final class Validate { + public static void notNull(Object arg, String name) { + if (arg == null) { + throw new NullPointerException("Argument '" + name + "' cannot be null"); + } + } + + public static void notEmpty(Collection container, String name) { + if (container.isEmpty()) { + throw new IllegalArgumentException("Container '" + name + "' cannot be empty"); + } + } + + public static void containsNoNulls(Collection container, String name) { + Validate.notNull(container, name); + for (T item : container) { + if (item == null) { + throw new NullPointerException("Container '" + name + "' cannot contain null values"); + } + } + } + + public static void containsNoNullOrEmpty(Collection container, String name) { + Validate.notNull(container, name); + for (String item : container) { + if (item == null) { + throw new NullPointerException("Container '" + name + "' cannot contain null values"); + } + if (item.length() == 0) { + throw new IllegalArgumentException("Container '" + name + "' cannot contain empty values"); + } + } + } + + public static void notEmptyAndContainsNoNulls(Collection container, String name) { + Validate.containsNoNulls(container, name); + Validate.notEmpty(container, name); + } + + public static void notNullOrEmpty(String arg, String name) { + if (Utility.isNullOrEmpty(arg)) { + throw new IllegalArgumentException("Argument '" + name + "' cannot be null or empty"); + } + } + + public static void oneOf(Object arg, String name, Object... values) { + for (Object value : values) { + if (value != null) { + if (value.equals(arg)) { + return; + } + } else { + if (arg == null) { + return; + } + } + } + throw new IllegalArgumentException("Argument '" + name + "' was not one of the allowed values"); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/WorkQueue.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/WorkQueue.java new file mode 100644 index 0000000000..f90f6acd7c --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/WorkQueue.java @@ -0,0 +1,221 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.internal; + +import com.facebook.Settings; + +import java.util.concurrent.Executor; + +class WorkQueue { + public static final int DEFAULT_MAX_CONCURRENT = 8; + + private final Object workLock = new Object(); + private WorkNode pendingJobs; + + private final int maxConcurrent; + private final Executor executor; + + private WorkNode runningJobs = null; + private int runningCount = 0; + + WorkQueue() { + this(DEFAULT_MAX_CONCURRENT); + } + + WorkQueue(int maxConcurrent) { + this(maxConcurrent, Settings.getExecutor()); + } + + WorkQueue(int maxConcurrent, Executor executor) { + this.maxConcurrent = maxConcurrent; + this.executor = executor; + } + + WorkItem addActiveWorkItem(Runnable callback) { + return addActiveWorkItem(callback, true); + } + + WorkItem addActiveWorkItem(Runnable callback, boolean addToFront) { + WorkNode node = new WorkNode(callback); + synchronized (workLock) { + pendingJobs = node.addToList(pendingJobs, addToFront); + } + + startItem(); + return node; + } + + void validate() { + synchronized (workLock) { + // Verify that all running items know they are running, and counts match + int count = 0; + + if (runningJobs != null) { + WorkNode walk = runningJobs; + do { + walk.verify(true); + count++; + walk = walk.getNext(); + } while (walk != runningJobs); + } + + assert runningCount == count; + } + } + + private void startItem() { + finishItemAndStartNew(null); + } + + private void finishItemAndStartNew(WorkNode finished) { + WorkNode ready = null; + + synchronized (workLock) { + if (finished != null) { + runningJobs = finished.removeFromList(runningJobs); + runningCount--; + } + + if (runningCount < maxConcurrent) { + ready = pendingJobs; // Head of the pendingJobs queue + if (ready != null) { + // The Queue reassignments are necessary since 'ready' might have been + // added / removed from the front of either queue, which changes its + // respective head. + pendingJobs = ready.removeFromList(pendingJobs); + runningJobs = ready.addToList(runningJobs, false); + runningCount++; + + ready.setIsRunning(true); + } + } + } + + if (ready != null) { + execute(ready); + } + } + + private void execute(final WorkNode node) { + executor.execute(new Runnable() { + @Override + public void run() { + try { + node.getCallback().run(); + } finally { + finishItemAndStartNew(node); + } + } + }); + } + + private class WorkNode implements WorkItem { + private final Runnable callback; + private WorkNode next; + private WorkNode prev; + private boolean isRunning; + + WorkNode(Runnable callback) { + this.callback = callback; + } + + @Override + public boolean cancel() { + synchronized (workLock) { + if (!isRunning()) { + pendingJobs = removeFromList(pendingJobs); + return true; + } + } + + return false; + } + + @Override + public void moveToFront() { + synchronized (workLock) { + if (!isRunning()) { + pendingJobs = removeFromList(pendingJobs); + pendingJobs = addToList(pendingJobs, true); + } + } + } + + @Override + public boolean isRunning() { + return isRunning; + } + + Runnable getCallback() { + return callback; + } + + WorkNode getNext() { + return next; + } + + void setIsRunning(boolean isRunning) { + this.isRunning = isRunning; + } + + WorkNode addToList(WorkNode list, boolean addToFront) { + assert next == null; + assert prev == null; + + if (list == null) { + list = next = prev = this; + } else { + next = list; + prev = list.prev; + next.prev = prev.next = this; + } + + return addToFront ? this : list; + } + + WorkNode removeFromList(WorkNode list) { + assert next != null; + assert prev != null; + + if (list == this) { + if (next == this) { + list = null; + } else { + list = next; + } + } + + next.prev = prev; + prev.next = next; + next = prev = null; + + return list; + } + + void verify(boolean shouldBeRunning) { + assert prev.next == this; + assert next.prev == this; + assert isRunning() == shouldBeRunning; + } + } + + interface WorkItem { + boolean cancel(); + boolean isRunning(); + void moveToFront(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/package-info.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/package-info.java new file mode 100644 index 0000000000..b3d145ce4e --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/internal/package-info.java @@ -0,0 +1,6 @@ +/** + * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of + * any of the classes in this package is unsupported, and they may be modified or removed without warning at + * any time. + */ +package com.facebook.internal; diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/CreateGraphObject.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/CreateGraphObject.java new file mode 100644 index 0000000000..92e7a2af51 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/CreateGraphObject.java @@ -0,0 +1,38 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Use this annotation setters in an interface that derives from + * GraphObject, if you wish to provide a setter that takes a primitive data type (e.g., String) + * or a List of primitive data types, but actually populates its underlying property with a + * new GraphObject with a property equal to the specified value (or a List of such GraphObjects). + * This is useful for providing "helper" setters to avoid requiring callers to instantiate a GraphObject + * just to set a single property on it (e.g., 'url' or 'id'). + * + * The String value provided to this annotation should be the name of the property that will be + * populated on the newly-created GraphObject using the value that was passed to the setter. + * + * This annotation has no effect if applied to a getter. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface CreateGraphObject { + String value(); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphLocation.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphLocation.java new file mode 100644 index 0000000000..9074a49766 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphLocation.java @@ -0,0 +1,130 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +/** + * Provides a strongly-typed representation of a Location as defined by the Graph API. + * + * Note that this interface is intended to be used with GraphObject.Factory + * and not implemented directly. + */ +public interface GraphLocation extends GraphObject { + /** + * Returns the street component of the location. + * + * @return the street component of the location, or null + */ + public String getStreet(); + + /** + * Sets the street component of the location. + * + * @param street + * the street component of the location, or null + */ + public void setStreet(String street); + + /** + * Gets the city component of the location. + * + * @return the city component of the location + */ + public String getCity(); + + /** + * Sets the city component of the location. + * + * @param city + * the city component of the location + */ + public void setCity(String city); + + /** + * Returns the state component of the location. + * + * @return the state component of the location + */ + public String getState(); + + /** + * Sets the state component of the location. + * + * @param state + * the state component of the location + */ + public void setState(String state); + + /** + * Returns the country component of the location. + * + * @return the country component of the location + */ + public String getCountry(); + + /** + * Sets the country component of the location + * + * @param country + * the country component of the location + */ + public void setCountry(String country); + + /** + * Returns the postal code component of the location. + * + * @return the postal code component of the location + */ + public String getZip(); + + /** + * Sets the postal code component of the location. + * + * @param zip + * the postal code component of the location + */ + public void setZip(String zip); + + /** + * Returns the latitude component of the location. + * + * @return the latitude component of the location + */ + public double getLatitude(); + + /** + * Sets the latitude component of the location. + * + * @param latitude + * the latitude component of the location + */ + public void setLatitude(double latitude); + + /** + * Returns the longitude component of the location. + * + * @return the longitude component of the location + */ + public double getLongitude(); + + /** + * Sets the longitude component of the location. + * + * @param longitude + * the longitude component of the location + */ + public void setLongitude(double longitude); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphMultiResult.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphMultiResult.java new file mode 100644 index 0000000000..dbcdbad3e6 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphMultiResult.java @@ -0,0 +1,32 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +/** + * Defines a GraphObject that represents the result of a query that returns multiple GraphObjects + * nested under a "data" property. + * + * Note that this interface is intended to be used with GraphObject.Factory + * and not implemented directly. + */ +public interface GraphMultiResult extends GraphObject { + /** + * Provides access to the GraphObjects that make up the result set. + * @return a list of GraphObjects + */ + public GraphObjectList getData(); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphObject.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphObject.java new file mode 100644 index 0000000000..c6684e6988 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphObject.java @@ -0,0 +1,839 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +import com.facebook.FacebookGraphObjectException; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.*; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * GraphObject is the primary interface used by the Facebook SDK for Android to represent objects in the Facebook + * Social Graph and the Facebook Open Graph (OG). It is the base interface for all typed access to graph objects + * in the SDK. No concrete classes implement GraphObject or its derived interfaces. Rather, they are implemented as + * proxies (see the {@link com.facebook.model.GraphObject.Factory Factory} class) that provide strongly-typed property + * getters and setters to access the underlying data. Since the primary use case for graph objects is sending and + * receiving them over the wire to/from Facebook services, they are represented as JSONObjects. No validation is done + * that a graph object is actually of a specific type -- any graph object can be treated as any GraphObject-derived + * interface, and the presence or absence of specific properties determines its suitability for use as that + * particular type of object. + *
+ */ +public interface GraphObject { + /** + * Returns a new proxy that treats this graph object as a different GraphObject-derived type. + * @param graphObjectClass the type of GraphObject to return + * @return a new instance of the GraphObject-derived-type that references the same underlying data + */ + T cast(Class graphObjectClass); + + /** + * Returns a Java Collections map of names and properties. Modifying the returned map modifies the + * inner JSON representation. + * @return a Java Collections map representing the GraphObject state + */ + Map asMap(); + + /** + * Gets the underlying JSONObject representation of this graph object. + * @return the underlying JSONObject representation of this graph object + */ + JSONObject getInnerJSONObject(); + + /** + * Gets a property of the GraphObject + * @param propertyName the name of the property to get + * @return the value of the named property + */ + Object getProperty(String propertyName); + + /** + * Gets a property of the GraphObject, cast to a particular GraphObject-derived interface. This gives some of + * the benefits of having a property getter defined to return a GraphObject-derived type without requiring + * explicit definition of an interface to define the getter. + * @param propertyName the name of the property to get + * @param graphObjectClass the GraphObject-derived interface to cast the property to + * @return + */ + T getPropertyAs(String propertyName, Class graphObjectClass); + + /** + * Gets a property of the GraphObject, cast to a a list of instances of a particular GraphObject-derived interface. + * This gives some of the benefits of having a property getter defined to return a GraphObject-derived type without + * requiring explicit definition of an interface to define the getter. + * @param propertyName the name of the property to get + * @param graphObjectClass the GraphObject-derived interface to cast the property to a list of + * @return + */ + GraphObjectList getPropertyAsList(String propertyName, Class graphObjectClass); + + /** + * Sets a property of the GraphObject + * @param propertyName the name of the property to set + * @param propertyValue the value of the named property to set + */ + void setProperty(String propertyName, Object propertyValue); + + /** + * Removes a property of the GraphObject + * @param propertyName the name of the property to remove + */ + void removeProperty(String propertyName); + + /** + * Creates proxies that implement GraphObject, GraphObjectList, and their derived types. These proxies allow access + * to underlying collections and name/value property bags via strongly-typed property getters and setters. + *

+ * This supports get/set properties that use primitive types, JSON types, Date, other GraphObject types, Iterable, + * Collection, List, and GraphObjectList. + */ + final class Factory { + private static final HashSet> verifiedGraphObjectClasses = new HashSet>(); + private static final SimpleDateFormat[] dateFormats = new SimpleDateFormat[] { + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US), new SimpleDateFormat("yyyy-MM-dd", Locale.US), }; + + // No objects of this type should exist. + private Factory() { + } + + /** + * Creates a GraphObject proxy that provides typed access to the data in an underlying JSONObject. + * @param json the JSONObject containing the data to be exposed + * @return a GraphObject that represents the underlying data + * + * @throws com.facebook.FacebookException + * If the passed in Class is not a valid GraphObject interface + */ + public static GraphObject create(JSONObject json) { + return create(json, GraphObject.class); + } + + /** + * Creates a GraphObject-derived proxy that provides typed access to the data in an underlying JSONObject. + * @param json the JSONObject containing the data to be exposed + * @param graphObjectClass the GraphObject-derived type to return + * @return a graphObjectClass that represents the underlying data + * + * @throws com.facebook.FacebookException + * If the passed in Class is not a valid GraphObject interface + */ + public static T create(JSONObject json, Class graphObjectClass) { + return createGraphObjectProxy(graphObjectClass, json); + } + + /** + * Creates a GraphObject proxy that initially contains no data. + * @return a GraphObject with no data + * + * @throws com.facebook.FacebookException + * If the passed in Class is not a valid GraphObject interface + */ + public static GraphObject create() { + return create(GraphObject.class); + } + + /** + * Creates a GraphObject-derived proxy that initially contains no data. + * @param graphObjectClass the GraphObject-derived type to return + * @return a graphObjectClass with no data + * + * @throws com.facebook.FacebookException + * If the passed in Class is not a valid GraphObject interface + */ + public static T create(Class graphObjectClass) { + return createGraphObjectProxy(graphObjectClass, new JSONObject()); + } + + /** + * Determines if two GraphObjects represent the same underlying graph object, based on their IDs. + * @param a a graph object + * @param b another graph object + * @return true if both graph objects have an ID and it is the same ID, false otherwise + */ + public static boolean hasSameId(GraphObject a, GraphObject b) { + if (a == null || b == null || !a.asMap().containsKey("id") || !b.asMap().containsKey("id")) { + return false; + } + if (a.equals(b)) { + return true; + } + Object idA = a.getProperty("id"); + Object idB = b.getProperty("id"); + if (idA == null || idB == null || !(idA instanceof String) || !(idB instanceof String)) { + return false; + } + return idA.equals(idB); + } + + /** + * Creates a GraphObjectList-derived proxy that provides typed access to the data in an underlying JSONArray. + * @param array the JSONArray containing the data to be exposed + * @param graphObjectClass the GraphObject-derived type to return + * @return a graphObjectClass that represents the underlying data + * + * @throws com.facebook.FacebookException + * If the passed in Class is not a valid GraphObject interface + */ + public static GraphObjectList createList(JSONArray array, Class graphObjectClass) { + return new GraphObjectListImpl(array, graphObjectClass); + } + + /** + * Creates a GraphObjectList-derived proxy that initially contains no data. + * @param graphObjectClass the GraphObject-derived type to return + * @return a GraphObjectList with no data + * + * @throws com.facebook.FacebookException + * If the passed in Class is not a valid GraphObject interface + */ + public static GraphObjectList createList(Class graphObjectClass) { + return createList(new JSONArray(), graphObjectClass); + } + + private static T createGraphObjectProxy(Class graphObjectClass, JSONObject state) { + verifyCanProxyClass(graphObjectClass); + + Class[] interfaces = new Class[] { graphObjectClass }; + GraphObjectProxy graphObjectProxy = new GraphObjectProxy(state, graphObjectClass); + + @SuppressWarnings("unchecked") + T graphObject = (T) Proxy.newProxyInstance(GraphObject.class.getClassLoader(), interfaces, graphObjectProxy); + + return graphObject; + } + + private static Map createGraphObjectProxyForMap(JSONObject state) { + Class[] interfaces = new Class[]{Map.class}; + GraphObjectProxy graphObjectProxy = new GraphObjectProxy(state, Map.class); + + @SuppressWarnings("unchecked") + Map graphObject = (Map) Proxy + .newProxyInstance(GraphObject.class.getClassLoader(), interfaces, graphObjectProxy); + + return graphObject; + } + + private static synchronized boolean hasClassBeenVerified(Class graphObjectClass) { + return verifiedGraphObjectClasses.contains(graphObjectClass); + } + + private static synchronized void recordClassHasBeenVerified(Class graphObjectClass) { + verifiedGraphObjectClasses.add(graphObjectClass); + } + + private static void verifyCanProxyClass(Class graphObjectClass) { + if (hasClassBeenVerified(graphObjectClass)) { + return; + } + + if (!graphObjectClass.isInterface()) { + throw new FacebookGraphObjectException("Factory can only wrap interfaces, not class: " + + graphObjectClass.getName()); + } + + Method[] methods = graphObjectClass.getMethods(); + for (Method method : methods) { + String methodName = method.getName(); + int parameterCount = method.getParameterTypes().length; + Class returnType = method.getReturnType(); + boolean hasPropertyNameOverride = method.isAnnotationPresent(PropertyName.class); + + if (method.getDeclaringClass().isAssignableFrom(GraphObject.class)) { + // Don't worry about any methods from GraphObject or one of its base classes. + continue; + } else if (parameterCount == 1 && returnType == Void.TYPE) { + if (hasPropertyNameOverride) { + // If a property override is present, it MUST be valid. We don't fallback + // to using the method name + if (!Utility.isNullOrEmpty(method.getAnnotation(PropertyName.class).value())) { + continue; + } + } else if (methodName.startsWith("set") && methodName.length() > 3) { + // Looks like a valid setter + continue; + } + } else if (parameterCount == 0 && returnType != Void.TYPE) { + if (hasPropertyNameOverride) { + // If a property override is present, it MUST be valid. We don't fallback + // to using the method name + if (!Utility.isNullOrEmpty(method.getAnnotation(PropertyName.class).value())) { + continue; + } + } else if (methodName.startsWith("get") && methodName.length() > 3) { + // Looks like a valid getter + continue; + } + } + + throw new FacebookGraphObjectException("Factory can't proxy method: " + method.toString()); + } + + recordClassHasBeenVerified(graphObjectClass); + } + + // If expectedType is a generic type, expectedTypeAsParameterizedType must be provided in order to determine + // generic parameter types. + static U coerceValueToExpectedType(Object value, Class expectedType, + ParameterizedType expectedTypeAsParameterizedType) { + if (value == null) { + if (boolean.class.equals(expectedType)) { + @SuppressWarnings("unchecked") + U result = (U) (Boolean) false; + return result; + } else if (char.class.equals(expectedType)) { + @SuppressWarnings("unchecked") + U result = (U) (Character) '\0'; + return result; + } else if (expectedType.isPrimitive()) { + @SuppressWarnings("unchecked") + U result = (U) (Number) 0; + return result; + } else { + return null; + } + } + + Class valueType = value.getClass(); + if (expectedType.isAssignableFrom(valueType)) { + @SuppressWarnings("unchecked") + U result = (U) value; + return result; + } + + if (expectedType.isPrimitive()) { + // If the result is a primitive, let the runtime succeed or fail at unboxing it. + @SuppressWarnings("unchecked") + U result = (U) value; + return result; + } + + if (GraphObject.class.isAssignableFrom(expectedType)) { + @SuppressWarnings("unchecked") + Class graphObjectClass = (Class) expectedType; + + // We need a GraphObject, but we don't have one. + if (JSONObject.class.isAssignableFrom(valueType)) { + // We can wrap a JSONObject as a GraphObject. + @SuppressWarnings("unchecked") + U result = (U) createGraphObjectProxy(graphObjectClass, (JSONObject) value); + return result; + } else if (GraphObject.class.isAssignableFrom(valueType)) { + // We can cast a GraphObject-derived class to another GraphObject-derived class. + @SuppressWarnings("unchecked") + U result = (U) ((GraphObject) value).cast(graphObjectClass); + return result; + } else { + throw new FacebookGraphObjectException("Can't create GraphObject from " + valueType.getName()); + } + } else if (Iterable.class.equals(expectedType) || Collection.class.equals(expectedType) + || List.class.equals(expectedType) || GraphObjectList.class.equals(expectedType)) { + if (expectedTypeAsParameterizedType == null) { + throw new FacebookGraphObjectException("can't infer generic type of: " + expectedType.toString()); + } + + Type[] actualTypeArguments = expectedTypeAsParameterizedType.getActualTypeArguments(); + + if (actualTypeArguments == null || actualTypeArguments.length != 1 + || !(actualTypeArguments[0] instanceof Class)) { + throw new FacebookGraphObjectException( + "Expect collection properties to be of a type with exactly one generic parameter."); + } + Class collectionGenericArgument = (Class) actualTypeArguments[0]; + + if (JSONArray.class.isAssignableFrom(valueType)) { + JSONArray jsonArray = (JSONArray) value; + @SuppressWarnings("unchecked") + U result = (U) createList(jsonArray, collectionGenericArgument); + return result; + } else { + throw new FacebookGraphObjectException("Can't create Collection from " + valueType.getName()); + } + } else if (String.class.equals(expectedType)) { + if (Double.class.isAssignableFrom(valueType) || + Float.class.isAssignableFrom(valueType)) { + @SuppressWarnings("unchecked") + U result = (U) String.format("%f", value); + return result; + } else if (Number.class.isAssignableFrom(valueType)) { + @SuppressWarnings("unchecked") + U result = (U) String.format("%d", value); + return result; + } + } else if (Date.class.equals(expectedType)) { + if (String.class.isAssignableFrom(valueType)) { + for (SimpleDateFormat format : dateFormats) { + try { + Date date = format.parse((String) value); + if (date != null) { + @SuppressWarnings("unchecked") + U result = (U) date; + return result; + } + } catch (ParseException e) { + // Keep going. + } + } + } + } + throw new FacebookGraphObjectException("Can't convert type" + valueType.getName() + " to " + + expectedType.getName()); + } + + static String convertCamelCaseToLowercaseWithUnderscores(String string) { + string = string.replaceAll("([a-z])([A-Z])", "$1_$2"); + return string.toLowerCase(Locale.US); + } + + private static Object getUnderlyingJSONObject(Object obj) { + if (obj == null) { + return null; + } + + Class objClass = obj.getClass(); + if (GraphObject.class.isAssignableFrom(objClass)) { + GraphObject graphObject = (GraphObject) obj; + return graphObject.getInnerJSONObject(); + } else if (GraphObjectList.class.isAssignableFrom(objClass)) { + GraphObjectList graphObjectList = (GraphObjectList) obj; + return graphObjectList.getInnerJSONArray(); + } else if (Iterable.class.isAssignableFrom(objClass)) { + JSONArray jsonArray = new JSONArray(); + Iterable iterable = (Iterable) obj; + for (Object o : iterable ) { + if (GraphObject.class.isAssignableFrom(o.getClass())) { + jsonArray.put(((GraphObject)o).getInnerJSONObject()); + } else { + jsonArray.put(o); + } + } + return jsonArray; + } + return obj; + } + + private abstract static class ProxyBase implements InvocationHandler { + // Pre-loaded Method objects for the methods in java.lang.Object + private static final String EQUALS_METHOD = "equals"; + private static final String TOSTRING_METHOD = "toString"; + + protected final STATE state; + + protected ProxyBase(STATE state) { + this.state = state; + } + + // Declared to return Object just to simplify implementation of proxy helpers. + protected final Object throwUnexpectedMethodSignature(Method method) { + throw new FacebookGraphObjectException(getClass().getName() + " got an unexpected method signature: " + + method.toString()); + } + + protected final Object proxyObjectMethods(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (methodName.equals(EQUALS_METHOD)) { + Object other = args[0]; + + if (other == null) { + return false; + } + + InvocationHandler handler = Proxy.getInvocationHandler(other); + if (!(handler instanceof GraphObjectProxy)) { + return false; + } + GraphObjectProxy otherProxy = (GraphObjectProxy) handler; + return this.state.equals(otherProxy.state); + } else if (methodName.equals(TOSTRING_METHOD)) { + return toString(); + } + + // For others, just defer to the implementation object. + return method.invoke(this.state, args); + } + + } + + private final static class GraphObjectProxy extends ProxyBase { + private static final String CLEAR_METHOD = "clear"; + private static final String CONTAINSKEY_METHOD = "containsKey"; + private static final String CONTAINSVALUE_METHOD = "containsValue"; + private static final String ENTRYSET_METHOD = "entrySet"; + private static final String GET_METHOD = "get"; + private static final String ISEMPTY_METHOD = "isEmpty"; + private static final String KEYSET_METHOD = "keySet"; + private static final String PUT_METHOD = "put"; + private static final String PUTALL_METHOD = "putAll"; + private static final String REMOVE_METHOD = "remove"; + private static final String SIZE_METHOD = "size"; + private static final String VALUES_METHOD = "values"; + private static final String CAST_METHOD = "cast"; + private static final String CASTTOMAP_METHOD = "asMap"; + private static final String GETPROPERTY_METHOD = "getProperty"; + private static final String GETPROPERTYAS_METHOD = "getPropertyAs"; + private static final String GETPROPERTYASLIST_METHOD = "getPropertyAsList"; + private static final String SETPROPERTY_METHOD = "setProperty"; + private static final String REMOVEPROPERTY_METHOD = "removeProperty"; + private static final String GETINNERJSONOBJECT_METHOD = "getInnerJSONObject"; + + private final Class graphObjectClass; + + public GraphObjectProxy(JSONObject state, Class graphObjectClass) { + super(state); + this.graphObjectClass = graphObjectClass; + } + + @Override + public String toString() { + return String.format("GraphObject{graphObjectClass=%s, state=%s}", graphObjectClass.getSimpleName(), state); + } + + @Override + public final Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Class declaringClass = method.getDeclaringClass(); + + if (declaringClass == Object.class) { + return proxyObjectMethods(proxy, method, args); + } else if (declaringClass == Map.class) { + return proxyMapMethods(method, args); + } else if (declaringClass == GraphObject.class) { + return proxyGraphObjectMethods(proxy, method, args); + } else if (GraphObject.class.isAssignableFrom(declaringClass)) { + return proxyGraphObjectGettersAndSetters(method, args); + } + + return throwUnexpectedMethodSignature(method); + } + + private final Object proxyMapMethods(Method method, Object[] args) { + String methodName = method.getName(); + if (methodName.equals(CLEAR_METHOD)) { + JsonUtil.jsonObjectClear(this.state); + return null; + } else if (methodName.equals(CONTAINSKEY_METHOD)) { + return this.state.has((String) args[0]); + } else if (methodName.equals(CONTAINSVALUE_METHOD)) { + return JsonUtil.jsonObjectContainsValue(this.state, args[0]); + } else if (methodName.equals(ENTRYSET_METHOD)) { + return JsonUtil.jsonObjectEntrySet(this.state); + } else if (methodName.equals(GET_METHOD)) { + return this.state.opt((String) args[0]); + } else if (methodName.equals(ISEMPTY_METHOD)) { + return this.state.length() == 0; + } else if (methodName.equals(KEYSET_METHOD)) { + return JsonUtil.jsonObjectKeySet(this.state); + } else if (methodName.equals(PUT_METHOD)) { + return setJSONProperty(args); + } else if (methodName.equals(PUTALL_METHOD)) { + Map map = null; + if (args[0] instanceof Map) { + @SuppressWarnings("unchecked") + Map castMap = (Map) args[0]; + map = castMap; + } else if (args[0] instanceof GraphObject) { + map = ((GraphObject) args[0]).asMap(); + } else { + return null; + } + JsonUtil.jsonObjectPutAll(this.state, map); + return null; + } else if (methodName.equals(REMOVE_METHOD)) { + this.state.remove((String) args[0]); + return null; + } else if (methodName.equals(SIZE_METHOD)) { + return this.state.length(); + } else if (methodName.equals(VALUES_METHOD)) { + return JsonUtil.jsonObjectValues(this.state); + } + + return throwUnexpectedMethodSignature(method); + } + + private final Object proxyGraphObjectMethods(Object proxy, Method method, Object[] args) { + String methodName = method.getName(); + if (methodName.equals(CAST_METHOD)) { + @SuppressWarnings("unchecked") + Class graphObjectClass = (Class) args[0]; + + if (graphObjectClass != null && + graphObjectClass.isAssignableFrom(this.graphObjectClass)) { + return proxy; + } + return Factory.createGraphObjectProxy(graphObjectClass, this.state); + } else if (methodName.equals(GETINNERJSONOBJECT_METHOD)) { + InvocationHandler handler = Proxy.getInvocationHandler(proxy); + GraphObjectProxy otherProxy = (GraphObjectProxy) handler; + return otherProxy.state; + } else if (methodName.equals(CASTTOMAP_METHOD)) { + return Factory.createGraphObjectProxyForMap(this.state); + } else if (methodName.equals(GETPROPERTY_METHOD)) { + return state.opt((String) args[0]); + } else if (methodName.equals(GETPROPERTYAS_METHOD)) { + Object value = state.opt((String) args[0]); + Class expectedType = (Class) args[1]; + + return coerceValueToExpectedType(value, expectedType, null); + } else if (methodName.equals(GETPROPERTYASLIST_METHOD)) { + Object value = state.opt((String) args[0]); + final Class expectedType = (Class) args[1]; + + ParameterizedType parameterizedType = new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[]{ expectedType }; + } + + @Override + public Type getOwnerType() { + return null; + } + + @Override + public Type getRawType() { + return GraphObjectList.class; + } + }; + return coerceValueToExpectedType(value, GraphObjectList.class, parameterizedType); + } else if (methodName.equals(SETPROPERTY_METHOD)) { + return setJSONProperty(args); + } else if (methodName.equals(REMOVEPROPERTY_METHOD)) { + this.state.remove((String) args[0]); + return null; + } + + return throwUnexpectedMethodSignature(method); + } + + private Object createGraphObjectsFromParameters(CreateGraphObject createGraphObject, Object value) { + if (createGraphObject != null && + !Utility.isNullOrEmpty(createGraphObject.value())) { + String propertyName = createGraphObject.value(); + if (List.class.isAssignableFrom(value.getClass())) { + GraphObjectList graphObjects = GraphObject.Factory.createList(GraphObject.class); + @SuppressWarnings("unchecked") + List values = (List)value; + for (Object obj : values) { + GraphObject graphObject = GraphObject.Factory.create(); + graphObject.setProperty(propertyName, obj); + graphObjects.add(graphObject); + } + + value = graphObjects; + } else { + GraphObject graphObject = GraphObject.Factory.create(); + graphObject.setProperty(propertyName, value); + + value = graphObject; + } + } + + return value; + } + + private final Object proxyGraphObjectGettersAndSetters(Method method, Object[] args) throws JSONException { + String methodName = method.getName(); + int parameterCount = method.getParameterTypes().length; + PropertyName propertyNameOverride = method.getAnnotation(PropertyName.class); + + String key = propertyNameOverride != null ? propertyNameOverride.value() : + convertCamelCaseToLowercaseWithUnderscores(methodName.substring(3)); + + // If it's a get or a set on a GraphObject-derived class, we can handle it. + if (parameterCount == 0) { + // Has to be a getter. ASSUMPTION: The GraphObject-derived class has been verified + Object value = this.state.opt(key); + + Class expectedType = method.getReturnType(); + + Type genericReturnType = method.getGenericReturnType(); + ParameterizedType parameterizedReturnType = null; + if (genericReturnType instanceof ParameterizedType) { + parameterizedReturnType = (ParameterizedType) genericReturnType; + } + + value = coerceValueToExpectedType(value, expectedType, parameterizedReturnType); + + return value; + } else if (parameterCount == 1) { + // Has to be a setter. ASSUMPTION: The GraphObject-derived class has been verified + CreateGraphObject createGraphObjectAnnotation = method.getAnnotation(CreateGraphObject.class); + Object value = createGraphObjectsFromParameters(createGraphObjectAnnotation, args[0]); + + // If this is a wrapped object, store the underlying JSONObject instead, in order to serialize + // correctly. + value = getUnderlyingJSONObject(value); + this.state.putOpt(key, value); + return null; + } + + return throwUnexpectedMethodSignature(method); + } + + private Object setJSONProperty(Object[] args) { + String name = (String) args[0]; + Object property = args[1]; + Object value = getUnderlyingJSONObject(property); + try { + state.putOpt(name, value); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + return null; + } + } + + private final static class GraphObjectListImpl extends AbstractList implements GraphObjectList { + private final JSONArray state; + private final Class itemType; + + public GraphObjectListImpl(JSONArray state, Class itemType) { + Validate.notNull(state, "state"); + Validate.notNull(itemType, "itemType"); + + this.state = state; + this.itemType = itemType; + } + + @Override + public String toString() { + return String.format("GraphObjectList{itemType=%s, state=%s}", itemType.getSimpleName(), state); + } + + @Override + public void add(int location, T object) { + // We only support adding at the end of the list, due to JSONArray restrictions. + if (location < 0) { + throw new IndexOutOfBoundsException(); + } else if (location < size()) { + throw new UnsupportedOperationException("Only adding items at the end of the list is supported."); + } + + put(location, object); + } + + @Override + public T set(int location, T object) { + checkIndex(location); + + T result = get(location); + put(location, object); + return result; + } + + @Override + public int hashCode() { + return state.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } else if (this == obj) { + return true; + } else if (getClass() != obj.getClass()) { + return false; + } + @SuppressWarnings("unchecked") + GraphObjectListImpl other = (GraphObjectListImpl) obj; + return state.equals(other.state); + } + + @SuppressWarnings("unchecked") + @Override + public T get(int location) { + checkIndex(location); + + Object value = state.opt(location); + + // Class expectedType = method.getReturnType(); + // Type genericType = method.getGenericReturnType(); + T result = (T) coerceValueToExpectedType(value, itemType, null); + + return result; + } + + @Override + public int size() { + return state.length(); + } + + @Override + public final GraphObjectList castToListOf(Class graphObjectClass) { + if (GraphObject.class.isAssignableFrom(itemType)) { + if (graphObjectClass.isAssignableFrom(itemType)) { + @SuppressWarnings("unchecked") + GraphObjectList result = (GraphObjectList)this; + return result; + } + + return createList(state, graphObjectClass); + } else { + throw new FacebookGraphObjectException("Can't cast GraphObjectCollection of non-GraphObject type " + + itemType); + } + } + + @Override + public final JSONArray getInnerJSONArray() { + return state; + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + private void checkIndex(int index) { + if (index < 0 || index >= state.length()) { + throw new IndexOutOfBoundsException(); + } + } + + private void put(int index, T obj) { + Object underlyingObject = getUnderlyingJSONObject(obj); + try { + state.put(index, underlyingObject); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphObjectList.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphObjectList.java new file mode 100644 index 0000000000..e4184fa34e --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphObjectList.java @@ -0,0 +1,47 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +import org.json.JSONArray; + +import java.util.List; + +/** + * GraphObjectList is the primary representation of a collection of graph objects in the Facebook SDK for Android. + * It is not implemented by any concrete classes, but rather by a proxy (see the {@link com.facebook.model.GraphObject.Factory Factory} + * class). A GraphObjectList can actually contain elements of any type, not just graph objects, but its principal + * use in the SDK is to contain types derived from GraphObject. + *
+ * + * @param the type of elements in the list + */ +public interface GraphObjectList extends List { + // cast method is only supported if T extends GraphObject + /** + * If T is derived from GraphObject, returns a new GraphObjectList exposing the same underlying data as a new + * GraphObject-derived type. + * @param graphObjectClass the GraphObject-derived type to return a list of + * @return a list representing the same underlying data, exposed as the new GraphObject-derived type + * @throws com.facebook.FacebookGraphObjectException if T does not derive from GraphObject + */ + public GraphObjectList castToListOf(Class graphObjectClass); + /** + * Gets the underlying JSONArray representation of the data. + * @return the underlying JSONArray representation of the data + */ + public JSONArray getInnerJSONArray(); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphPlace.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphPlace.java new file mode 100644 index 0000000000..1d624b7f1f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphPlace.java @@ -0,0 +1,69 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +/** + * Provides a strongly-typed representation of a Place as defined by the Graph API. + * + * Note that this interface is intended to be used with GraphObject.Factory + * and not implemented directly. + */ +public interface GraphPlace extends GraphObject { + /** + * Returns the ID of the place. + * @return the ID of the place + */ + public String getId(); + /** + * Sets the ID of the place. + * @param id the ID of the place + */ + public void setId(String id); + + /** + * Returns the name of the place. + * @return the name of the place + */ + public String getName(); + /** + * Sets the name of the place. + * @param name the name of the place + */ + public void setName(String name); + + /** + * Returns the category of the place. + * @return the category of the place + */ + public String getCategory(); + /** + * Sets the category of the place. + * @param category the category of the place + */ + public void setCategory(String category); + + /** + * Returns the location of the place. + * @return the location of the place + */ + public GraphLocation getLocation(); + /** + * Sets the location of the place. + * @param location the location of the place + */ + public void setLocation(GraphLocation location); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphUser.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphUser.java new file mode 100644 index 0000000000..51db95d4e3 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/GraphUser.java @@ -0,0 +1,125 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +/** + * Provides a strongly-typed representation of a User as defined by the Graph API. + * + * Note that this interface is intended to be used with GraphObject.Factory + * and not implemented directly. + */ +public interface GraphUser extends GraphObject { + /** + * Returns the ID of the user. + * @return the ID of the user + */ + public String getId(); + /** + * Sets the ID of the user. + * @param id the ID of the user + */ + public void setId(String id); + + /** + * Returns the name of the user. + * @return the name of the user + */ + public String getName(); + /** + * Sets the name of the user. + * @param name the name of the user + */ + public void setName(String name); + + /** + * Returns the first name of the user. + * @return the first name of the user + */ + public String getFirstName(); + /** + * Sets the first name of the user. + * @param firstName the first name of the user + */ + public void setFirstName(String firstName); + + /** + * Returns the middle name of the user. + * @return the middle name of the user + */ + public String getMiddleName(); + /** + * Sets the middle name of the user. + * @param middleName the middle name of the user + */ + public void setMiddleName(String middleName); + + /** + * Returns the last name of the user. + * @return the last name of the user + */ + public String getLastName(); + /** + * Sets the last name of the user. + * @param lastName the last name of the user + */ + public void setLastName(String lastName); + + /** + * Returns the Facebook URL of the user. + * @return the Facebook URL of the user + */ + public String getLink(); + /** + * Sets the Facebook URL of the user. + * @param link the Facebook URL of the user + */ + public void setLink(String link); + + /** + * Returns the Facebook username of the user. + * @return the Facebook username of the user + */ + public String getUsername(); + /** + * Sets the Facebook username of the user. + * @param username the Facebook username of the user + */ + public void setUsername(String username); + + /** + * Returns the birthday of the user. + * @return the birthday of the user + */ + public String getBirthday(); + /** + * Sets the birthday of the user. + * @param birthday the birthday of the user + */ + public void setBirthday(String birthday); + + /** + * Returns the current place of the user. + * @return the current place of the user + */ + public GraphPlace getLocation(); + + /** + * Sets the current place of the user. + * @param location the current place of the user + */ + public void setLocation(GraphPlace location); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/JsonUtil.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/JsonUtil.java new file mode 100644 index 0000000000..6d20122367 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/JsonUtil.java @@ -0,0 +1,122 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +import android.annotation.SuppressLint; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.*; + +class JsonUtil { + static void jsonObjectClear(JSONObject jsonObject) { + @SuppressWarnings("unchecked") + Iterator keys = (Iterator) jsonObject.keys(); + while (keys.hasNext()) { + keys.next(); + keys.remove(); + } + } + + static boolean jsonObjectContainsValue(JSONObject jsonObject, Object value) { + @SuppressWarnings("unchecked") + Iterator keys = (Iterator) jsonObject.keys(); + while (keys.hasNext()) { + Object thisValue = jsonObject.opt(keys.next()); + if (thisValue != null && thisValue.equals(value)) { + return true; + } + } + return false; + } + + private final static class JSONObjectEntry implements Map.Entry { + private final String key; + private final Object value; + + JSONObjectEntry(String key, Object value) { + this.key = key; + this.value = value; + } + + @SuppressLint("FieldGetter") + @Override + public String getKey() { + return this.key; + } + + @Override + public Object getValue() { + return this.value; + } + + @Override + public Object setValue(Object object) { + throw new UnsupportedOperationException("JSONObjectEntry is immutable"); + } + + } + + static Set> jsonObjectEntrySet(JSONObject jsonObject) { + HashSet> result = new HashSet>(); + + @SuppressWarnings("unchecked") + Iterator keys = (Iterator) jsonObject.keys(); + while (keys.hasNext()) { + String key = keys.next(); + Object value = jsonObject.opt(key); + result.add(new JSONObjectEntry(key, value)); + } + + return result; + } + + static Set jsonObjectKeySet(JSONObject jsonObject) { + HashSet result = new HashSet(); + + @SuppressWarnings("unchecked") + Iterator keys = (Iterator) jsonObject.keys(); + while (keys.hasNext()) { + result.add(keys.next()); + } + + return result; + } + + static void jsonObjectPutAll(JSONObject jsonObject, Map map) { + Set> entrySet = map.entrySet(); + for (Map.Entry entry : entrySet) { + try { + jsonObject.putOpt(entry.getKey(), entry.getValue()); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + } + + static Collection jsonObjectValues(JSONObject jsonObject) { + ArrayList result = new ArrayList(); + + @SuppressWarnings("unchecked") + Iterator keys = (Iterator) jsonObject.keys(); + while (keys.hasNext()) { + result.add(jsonObject.opt(keys.next())); + } + + return result; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/OpenGraphAction.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/OpenGraphAction.java new file mode 100644 index 0000000000..d82d34a803 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/OpenGraphAction.java @@ -0,0 +1,306 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +import org.json.JSONObject; + +import java.util.Date; +import java.util.List; + +/** + * Provides a strongly-typed representation of an Open Graph Action. + * For more documentation of OG Actions, see: https://developers.facebook.com/docs/opengraph/actions/ + * + * Note that this interface is intended to be used with GraphObject.Factory or OpenGraphAction.Factory + * and not implemented directly. + */ +public interface OpenGraphAction extends GraphObject { + /** + * Gets the ID of the action. + * @return the ID + */ + String getId(); + + /** + * Sets the ID of the action. + * @param id the ID + */ + void setId(String id); + + /** + * Gets the type of the action, which is a string in the form "mynamespace:mytype". + * @return the type + */ + String getType(); + + /** + * Sets the type of the action, which is a string in the form "mynamespace:mytype". + * @param type the type + */ + void setType(String type); + + /** + * Gets the start time of the action. + * @return the start time + */ + Date getStartTime(); + + /** + * Sets the start time of the action. + * @param startTime the start time + */ + void setStartTime(Date startTime); + + /** + * Gets the end time of the action. + * @return the end time + */ + Date getEndTime(); + + /** + * Sets the end time of the action. + * @param endTime the end time + */ + void setEndTime(Date endTime); + + /** + * Gets the time the action was published, if any. + * @return the publish time + */ + Date getPublishTime(); + + /** + * Sets the time the action was published. + * @param publishTime the publish time + */ + void setPublishTime(Date publishTime); + + /** + * Gets the time the action was created. + * @return the creation time + */ + public Date getCreatedTime(); + + /** + * Sets the time the action was created. + * @param createdTime the creation time + */ + public void setCreatedTime(Date createdTime); + + /** + * Gets the time the action expires at. + * @return the expiration time + */ + public Date getExpiresTime(); + + /** + * Sets the time the action expires at. + * @param expiresTime the expiration time + */ + public void setExpiresTime(Date expiresTime); + + /** + * Gets the unique string which will be passed to the OG Action owner's website + * when a user clicks through this action on Facebook. + * @return the ref string + */ + String getRef(); + + /** + * Sets the unique string which will be passed to the OG Action owner's website + * when a user clicks through this action on Facebook. + * @param ref the ref string + */ + void setRef(String ref); + + /** + * Gets the message assoicated with the action. + * @return the message + */ + String getMessage(); + + /** + * Sets the message associated with the action. + * @param message the message + */ + void setMessage(String message); + + /** + * Gets the place where the action took place. + * @return the place + */ + GraphPlace getPlace(); + + /** + * Sets the place where the action took place. + * @param place the place + */ + void setPlace(GraphPlace place); + + /** + * Gets the list of profiles that were tagged in the action. + * @return the profiles that were tagged in the action + */ + GraphObjectList getTags(); + + /** + * Sets the list of profiles that were tagged in the action. + * @param tags the profiles that were tagged in the action + */ + void setTags(List tags); + + /** + * Gets the images that were associated with the action. + * @return the images + */ + List getImage(); + + /** + * Sets the images that were associated with the action. + * @param image the images + */ + void setImage(List image); + + /** + * Sets the images associated with the Open Graph action by specifying their URLs. This is a helper + * that will create GraphObjects with the correct URLs and populate the property with those objects. + * @param urls the URLs + */ + @CreateGraphObject("url") + @PropertyName("image") + void setImageUrls(List urls); + + /** + * Gets the from-user associated with the action. + * @return the user + */ + GraphUser getFrom(); + + /** + * Sets the from-user associated with the action. + * @param from the from-user + */ + void setFrom(GraphUser from); + + /** + * Gets the 'likes' that have been performed on this action. + * @return the likes + */ + public JSONObject getLikes(); + + /** + * Sets the 'likes' that have been performed on this action. + * @param likes the likes + */ + public void setLikes(JSONObject likes); + + /** + * Gets the application that created this action. + * @return the application + */ + GraphObject getApplication(); + + /** + * Sets the application that created this action. + * @param application the application + */ + void setApplication(GraphObject application); + + /** + * Gets the comments that have been made on this action. + * @return the comments + */ + public JSONObject getComments(); + + /** + * Sets the comments that have been made on this action. + * @param comments the comments + */ + void setComments(JSONObject comments); + + /** + * Gets the type-specific data for this action; for instance, any properties + * referencing Open Graph objects will appear under here. + * @return a GraphObject representing the type-specific data + */ + GraphObject getData(); + + /** + * Sets the type-specific data for this action. + * @param data a GraphObject representing the type-specific data + */ + void setData(GraphObject data); + + + /** + * Gets whether the action has been explicitly shared by the user. See + * Explicit Sharing for + * more information. + * @return true if this action was explicitly shared + */ + @PropertyName("fb:explicitly_shared") + boolean getExplicitlyShared(); + + /** + * Sets whether the action has been explicitly shared by the user. See + * Explicit Sharing for + * more information. You should only specify this property if explicit sharing has been enabled for an + * Open Graph action type. + * @param explicitlyShared true if this action was explicitly shared + */ + @PropertyName("fb:explicitly_shared") + void setExplicitlyShared(boolean explicitlyShared); + + /** + * Exposes helpers for creating instances of OpenGraphAction. + */ + final class Factory { + /** + * Creates an OpenGraphAction suitable for posting via, e.g., a native Share dialog. + * @return an OpenGraphAction + */ + @Deprecated + public static OpenGraphAction createForPost() { + return createForPost(OpenGraphAction.class, null); + } + + /** + * Creates an OpenGraphAction suitable for posting via, e.g., a native Share dialog. + * @param type the Open Graph action type for the action, or null if it will be specified later + * @return an OpenGraphAction + */ + public static OpenGraphAction createForPost(String type) { + return createForPost(OpenGraphAction.class, type); + } + + /** + * Creates an OpenGraphAction suitable for posting via, e.g., a native Share dialog. + * @param type the Open Graph action type for the action, or null if it will be specified later + * @param graphObjectClass the OpenGraphAction-derived type to return + * @return an OpenGraphAction + */ + public static T createForPost(Class graphObjectClass, String type) { + T object = GraphObject.Factory.create(graphObjectClass); + + if (type != null) { + object.setType(type); + } + + return object; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/OpenGraphObject.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/OpenGraphObject.java new file mode 100644 index 0000000000..285de4198f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/OpenGraphObject.java @@ -0,0 +1,318 @@ +package com.facebook.model; + +import com.facebook.internal.NativeProtocol; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Provides a strongly-typed representation of an Open Graph Object. + * For more documentation of OG Objects, see: https://developers.facebook.com/docs/opengraph/using-object-api/ + * + * Note that this interface is intended to be used with GraphObject.Factory or OpenGraphObject.Factory + * and not implemented directly. + */ +public interface OpenGraphObject extends GraphObject { + /** + * Gets the ID of the object. + * @return the ID + */ + String getId(); + + /** + * Sets the ID of the object. + * @param id the ID + */ + void setId(String id); + + /** + * Gets the type of the object, which is a string in the form "mynamespace:mytype". + * @return the type + */ + String getType(); + + /** + * Sets the type of the object, which is a string in the form "mynamespace:mytype". + * @param type the type + */ + void setType(String type); + + /** + * Gets the URL associated with the Open Graph object. + * @return the URL + */ + String getUrl(); + + /** + * Sets the URL associated with the Open Graph object. + * @param url the URL + */ + void setUrl(String url); + + /** + * Gets the title of the Open Graph object. + * @return the title + */ + String getTitle(); + + /** + * Sets the title of the Open Graph object. + * @param title the title + */ + void setTitle(String title); + + + /** + * Gets the description of the Open Graph object. + * @return the description + */ + String getDescription(); + + /** + * Sets the description of the Open Graph Object + * @param description the description + */ + void setDescription(String description); + + /** + * Gets the images associated with the Open Graph object. + * @return the images + */ + GraphObjectList getImage(); + + /** + * Sets the images associated with the Open Graph object. + * @param images the images + */ + void setImage(GraphObjectList images); + + /** + * Sets the images associated with the Open Graph object by specifying their URLs. This is a helper + * that will create GraphObjects with the correct URLs and populate the property with those objects. + * @param urls the URLs + */ + @CreateGraphObject("url") + @PropertyName("image") + void setImageUrls(List urls); + + /** + * Gets the videos associated with the Open Graph object. + * @return the videos + */ + GraphObjectList getVideo(); + + /** + * Sets the videos associated with the Open Graph object. + * @param videos the videos + */ + void setVideo(GraphObjectList videos); + + /** + * Gets the audio associated with the Open Graph object. + * @return the audio + */ + GraphObjectList getAudio(); + + /** + * Sets the audio associated with the Open Graph object. + * @param audios the audio + */ + void setAudio(GraphObjectList audios); + + /** + * Gets the "determiner" for the Open Graph object. This is the word such as "a", "an", or "the" that will + * appear before the title of the object. + * @return the determiner string + */ + String getDeterminer(); + + /** + * Sets the "determiner" for the Open Graph object. This is the word such as "a", "an", or "the" that will + * appear before the title of the object. + * @param determiner the determiner string + */ + void setDeterminer(String determiner); + + /** + * Gets the list of related resources for the Open Graph object. + * @return a list of URLs of related resources + */ + List getSeeAlso(); + + /** + * Sets the list of related resources for the Open Graph object. + * @param seeAlso a list of URLs of related resources + */ + void setSeeAlso(List seeAlso); + + /** + * Gets the name of the site hosting the Open Graph object, if any. + * @return the name of the site + */ + String getSiteName(); + + /** + * Sets the name of the site hosting the Open Graph object. + * @param siteName the name of the site + */ + void setSiteName(String siteName); + + /** + * Gets the date and time the Open Graph object was created. + * @return the creation time + */ + Date getCreatedTime(); + + /** + * Sets the date and time the Open Graph object was created. + * @param createdTime the creation time + */ + void setCreatedTime(Date createdTime); + + /** + * Gets the date and time the Open Graph object was last updated. + * @return the update time + */ + Date getUpdatedTime(); + + /** + * Sets the date and time the Open Graph object was last updated. + * @param updatedTime the update time + */ + void setUpdatedTime(Date updatedTime); + + /** + * Gets the application that created this object. + * @return the application + */ + GraphObject getApplication(); + + /** + * Sets the application that created this object. + * @param application the application + */ + void setApplication(GraphObject application); + + /** + * Gets whether the Open Graph object was created by scraping a Web resource or not. + * @return true if the Open Graph object was created by scraping the Web, false if not + */ + boolean getIsScraped(); + + /** + * Sets whether the Open Graph object was created by scraping a Web resource or not. + * @param isScraped true if the Open Graph object was created by scraping the Web, false if not + */ + void setIsScraped(boolean isScraped); + + /** + * Gets the Open Graph action which was created when this Open Graph action was posted, if it is a user-owned + * object, otherwise null. The post action controls the privacy of this object. + * @return the ID of the post action, if any, or null + */ + String getPostActionId(); + + /** + * Sets the Open Graph action which was created when this Open Graph action was posted, if it is a user-owned + * object, otherwise null. The post action controls the privacy of this object. + * @param postActionId the ID of the post action, if any, or null + */ + void setPostActionId(String postActionId); + + /** + * Gets the type-specific properties of the Open Graph object, if any. Any custom properties that are defined on an + * application-defined Open Graph object type will appear here. + * @return a GraphObject containing the type-specific properties + */ + GraphObject getData(); + + /** + * Sets the type-specific properties of the Open Graph object, if any. Any custom properties that are defined on an + * application-defined Open Graph object type will appear here. + * @param data a GraphObject containing the type-specific properties + */ + void setData(GraphObject data); + + /** + * Gets whether the object represents a new object that should be created as part of publishing via, e.g., the + * native Share dialog. This flag has no effect on explicit publishing of an action via, e.g., a POST to the + * '/me/objects/object_type' endpoint. + * @return true if the native Share dialog should create the object as part of publishing an action, false if not + */ + @PropertyName(NativeProtocol.OPEN_GRAPH_CREATE_OBJECT_KEY) + boolean getCreateObject(); + + /** + * Sets whether the object represents a new object that should be created as part of publishing via, e.g., the + * native Share dialog. This flag has no effect on explicit publishing of an action via, e.g., a POST to the + * '/me/objects/object_type' endpoint. + * @param createObject true if the native Share dialog should create the object as part of publishing an action, + * false if not + */ + @PropertyName(NativeProtocol.OPEN_GRAPH_CREATE_OBJECT_KEY) + void setCreateObject(boolean createObject); + + /** + * Exposes helpers for creating instances of OpenGraphObject. + */ + final class Factory { + /** + * Creates an OpenGraphObject suitable for posting via, e.g., a native Share dialog. The object will have + * no properties other than a 'create_object' and 'data' property, ready to be populated. + * @param type the Open Graph object type for the object, or null if it will be specified later + * @return an OpenGraphObject + */ + public static OpenGraphObject createForPost(String type) { + return createForPost(OpenGraphObject.class, type); + } + + /** + * Creates an OpenGraphObject suitable for posting via, e.g., a native Share dialog. The object will have + * no properties other than a 'create_object' and 'data' property, ready to be populated. + * @param graphObjectClass the OpenGraphObject-derived type to return + * @param type the Open Graph object type for the object, or null if it will be specified later + * @return an OpenGraphObject + */ + public static T createForPost(Class graphObjectClass, String type) { + return createForPost(graphObjectClass, type, null, null, null, null); + } + + /** + * Creates an OpenGraphObject suitable for posting via, e.g., a native Share dialog. The object will have + * the specified properties, plus a 'create_object' and 'data' property, ready to be populated. + * @param graphObjectClass the OpenGraphObject-derived type to return + * @param type the Open Graph object type for the object, or null if it will be specified later + * @param title the title of the object, or null if it will be specified later + * @param imageUrl the URL of an image associated with the object, or null + * @param url the URL associated with the object, or null + * @param description the description of the object, or null + * @return an OpenGraphObject + */ + public static T createForPost(Class graphObjectClass, String type, String title, + String imageUrl, String url, String description) { + T object = GraphObject.Factory.create(graphObjectClass); + + if (type != null) { + object.setType(type); + } + if (title != null) { + object.setTitle(title); + } + if (imageUrl != null) { + object.setImageUrls(Arrays.asList(imageUrl)); + } + if (url != null) { + object.setUrl(url); + } + if (description != null) { + object.setDescription(description); + } + + object.setCreateObject(true); + object.setData(GraphObject.Factory.create()); + + return object; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/PropertyName.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/PropertyName.java new file mode 100644 index 0000000000..1e705fe6f4 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/model/PropertyName.java @@ -0,0 +1,34 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.model; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Use this annotation on getters and setters in an interface that derives from + * GraphObject, if you wish to override the default property name that is inferred + * from the name of the method. + * + * If this annotation is specified on a method, it must contain a non-empty String + * value that represents the name of the property that the method is a getter or setter + * for. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface PropertyName { + String value(); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FacebookDialog.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FacebookDialog.java new file mode 100644 index 0000000000..1322228ca3 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FacebookDialog.java @@ -0,0 +1,1637 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.app.Fragment; +import com.facebook.*; +import com.facebook.internal.AnalyticsEvents; +import com.facebook.internal.NativeProtocol; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; +import com.facebook.model.*; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.util.*; + +/* + * Provides an interface for presenting dialogs provided by the Facebook application for Android. This class + * provides builders that present a strongly-typed interface to generate properly-formed Intents for launching + * the appropriate Activities provided by the Facebook application. + */ +public class FacebookDialog { + + public static final String COMPLETION_GESTURE_CANCEL = "cancel"; + + private static final String EXTRA_DIALOG_COMPLETE_KEY = "com.facebook.platform.extra.DID_COMPLETE"; + private static final String EXTRA_DIALOG_COMPLETION_GESTURE_KEY = + "com.facebook.platform.extra.COMPLETION_GESTURE"; + private static final String EXTRA_DIALOG_COMPLETION_ID_KEY = "com.facebook.platform.extra.POST_ID"; + + private static NativeAppCallAttachmentStore attachmentStore; + + /** + * Defines a callback interface that will be called when the user completes interacting with a Facebook + * dialog, or if an error occurs. + */ + public interface Callback { + /** + * Called when the user completes interacting with a Facebook dialog. + * + * @param pendingCall a PendingCall containing the call ID and original Intent used to launch the dialog + * @param data a Bundle containing the results of the dialog, whose contents will vary depending on the + * type of dialog being displayed. + */ + void onComplete(PendingCall pendingCall, Bundle data); + + /** + * Called if an error occurred while presenting a Facebook dialog. + * + * @param pendingCall a PendingCall containing the call ID and original Intent used to launch the dialog + * @param error the error that occurred + * @param data the full set of extras associated with the activity result + */ + void onError(PendingCall pendingCall, Exception error, Bundle data); + } + + private interface DialogFeature { + String getAction(); + int getMinVersion(); + } + + /** + * Defines a set of features that may be supported by the native Share dialog exposed by the Facebook application. + * As additional features are added, these flags may be passed to + * {@link FacebookDialog#canPresentShareDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.ShareDialogFeature...)} + * to determine whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features, which in turn may be used to determine which UI, etc., to present to the + * user. + */ + public enum ShareDialogFeature implements DialogFeature { + /** + * Indicates whether the native Share dialog itself is supported by the installed version of the + * Facebook application. + */ + SHARE_DIALOG(NativeProtocol.PROTOCOL_VERSION_20130618), + /** + * Indicates whether the native Share dialog supports sharing of photo images. + */ + PHOTOS(NativeProtocol.PROTOCOL_VERSION_20140204), + ; + + private int minVersion; + + private ShareDialogFeature(int minVersion) { + this.minVersion = minVersion; + } + + /** + * This method is for internal use only. + */ + public String getAction() { + return NativeProtocol.ACTION_FEED_DIALOG; + } + + /** + * This method is for internal use only. + */ + public int getMinVersion() { + return minVersion; + } + } + + /** + * Defines a set of features that may be supported by the native Message dialog exposed by the Facebook Messenger application. + * As additional features are added, these flags may be passed to + * {@link FacebookDialog#canPresentMessageDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.MessageDialogFeature...)} + * to determine whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features, which in turn may be used to determine which UI, etc., to present to the + * user. + */ + public enum MessageDialogFeature implements DialogFeature { + /** + * Indicates whether the native Message dialog itself is supported by the installed version of the + * Facebook application. + */ + MESSAGE_DIALOG(NativeProtocol.PROTOCOL_VERSION_20140204), + /** + * Indicates whether the native Message dialog supports sharing of photo images. + */ + PHOTOS(NativeProtocol.PROTOCOL_VERSION_20140324), + ; + + private int minVersion; + + private MessageDialogFeature(int minVersion) { + this.minVersion = minVersion; + } + + /** + * This method is for internal use only. + */ + public String getAction() { + return NativeProtocol.ACTION_MESSAGE_DIALOG; + } + + /** + * This method is for internal use only. + */ + public int getMinVersion() { + return minVersion; + } + } + + + /** + * Defines a set of features that may be supported by the native Open Graph dialogs exposed by the Facebook + * application. As additional features are added, these flags may be passed to + * {@link FacebookDialog#canPresentOpenGraphActionDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.OpenGraphActionDialogFeature...)} + * to determine whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features, which in turn may be used to determine which UI, etc., to present to the + * user. + */ + public enum OpenGraphActionDialogFeature implements DialogFeature { + /** + * Indicates whether the native Open Graph action dialog itself is supported by the installed version of the + * Facebook application. + */ + OG_ACTION_DIALOG(NativeProtocol.PROTOCOL_VERSION_20130618); + + private int minVersion; + + private OpenGraphActionDialogFeature(int minVersion) { + this.minVersion = minVersion; + } + + /** + * This method is for internal use only. + */ + public String getAction() { + return NativeProtocol.ACTION_OGACTIONPUBLISH_DIALOG; + } + + /** + * This method is for internal use only. + */ + public int getMinVersion() { + return minVersion; + } + } + + /** + * Defines a set of features that may be supported by the native Open Graph Message dialogs exposed by the Facebook + * application. As additional features are added, these flags may be passed to + * {@link FacebookDialog#canPresentOpenGraphMessageDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.OpenGraphMessageDialogFeature...)} + * to determine whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features, which in turn may be used to determine which UI, etc., to present to the + * user. + */ + public enum OpenGraphMessageDialogFeature implements DialogFeature { + /** + * Indicates whether the native Open Graph Message dialog itself is supported by the installed version of the + * Messenger application. + */ + OG_MESSAGE_DIALOG(NativeProtocol.PROTOCOL_VERSION_20140204); + + private int minVersion; + + private OpenGraphMessageDialogFeature(int minVersion) { + this.minVersion = minVersion; + } + + /** + * This method is for internal use only. + */ + public String getAction() { + return NativeProtocol.ACTION_OGMESSAGEPUBLISH_DIALOG; + } + + /** + * This method is for internal use only. + */ + public int getMinVersion() { + return minVersion; + } + } + + interface OnPresentCallback { + void onPresent(Context context) throws Exception; + } + + /** + * Determines whether the native dialog completed normally (without error or exception). + * + * @param result the bundle passed back to onActivityResult + * @return true if the native dialog completed normally + */ + public static boolean getNativeDialogDidComplete(Bundle result) { + return result.getBoolean(EXTRA_DIALOG_COMPLETE_KEY, false); + } + + /** + * Returns the gesture with which the user completed the native dialog. This is only returned if the + * user has previously authorized the calling app with basic permissions. + * + * @param result the bundle passed back to onActivityResult + * @return "post" or "cancel" as the completion gesture + */ + public static String getNativeDialogCompletionGesture(Bundle result) { + return result.getString(EXTRA_DIALOG_COMPLETION_GESTURE_KEY); + } + + /** + * Returns the id of the published post. This is only returned if the user has previously given the + * app publish permissions. + * + * @param result the bundle passed back to onActivityResult + * @return the id of the published post + */ + public static String getNativeDialogPostId(Bundle result) { + return result.getString(EXTRA_DIALOG_COMPLETION_ID_KEY); + } + + private Activity activity; + private Fragment fragment; + private PendingCall appCall; + private OnPresentCallback onPresentCallback; + + private FacebookDialog(Activity activity, Fragment fragment, PendingCall appCall, + OnPresentCallback onPresentCallback) { + this.activity = activity; + this.fragment = fragment; + this.appCall = appCall; + this.onPresentCallback = onPresentCallback; + } + + /** + * Launches an activity in the Facebook application to present the desired dialog. This method returns a + * PendingCall that contains a unique ID associated with this call to the Facebook application. In general, + * a calling Activity should use UiLifecycleHelper to handle incoming activity results, in order to ensure + * proper processing of the results from this dialog. + * + * @return a PendingCall containing the unique call ID corresponding to this call to the Facebook application + */ + public PendingCall present() { + logDialogActivity(activity, fragment, getEventName(appCall.getRequestIntent()), + AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_COMPLETED); + + if (onPresentCallback != null) { + try { + onPresentCallback.onPresent(activity); + } catch (Exception e) { + throw new FacebookException(e); + } + } + + if (fragment != null) { + fragment.startActivityForResult(appCall.getRequestIntent(), appCall.getRequestCode()); + } else { + activity.startActivityForResult(appCall.getRequestIntent(), appCall.getRequestCode()); + } + return appCall; + } + + /** + * Parses the results of a dialog activity and calls the appropriate method on the provided Callback. + * + * @param context the Context that is handling the activity result + * @param appCall an PendingCall containing the call ID and original Intent used to launch the dialog + * @param requestCode the request code for the activity result + * @param data the result Intent + * @param callback a callback to call after parsing the results + * @return true if the activity result was handled, false if not + */ + public static boolean handleActivityResult(Context context, PendingCall appCall, int requestCode, Intent data, + Callback callback) { + if (requestCode != appCall.getRequestCode()) { + return false; + } + + if (attachmentStore != null) { + attachmentStore.cleanupAttachmentsForCall(context, appCall.getCallId()); + } + + if (callback != null) { + if (NativeProtocol.isErrorResult(data)) { + Exception error = NativeProtocol.getErrorFromResult(data); + callback.onError(appCall, error, data.getExtras()); + } else { + callback.onComplete(appCall, data.getExtras()); + } + } + + return true; + } + + /** + * Determines whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features of the native Share dialog, which in turn may be used to determine + * which UI, etc., to present to the user. + * + * @param context the calling Context + * @param features zero or more features to check for; {@link ShareDialogFeature#SHARE_DIALOG} is implicitly checked + * if not explicitly specified + * @return true if all of the specified features are supported by the currently installed version of the + * Facebook application; false if any of the features are not supported + */ + public static boolean canPresentShareDialog(Context context, ShareDialogFeature... features) { + return handleCanPresent(context, EnumSet.of(ShareDialogFeature.SHARE_DIALOG, features)); + } + + /** + * Determines whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features of the native Message dialog, which in turn may be used to determine + * which UI, etc., to present to the user. + * + * @param context the calling Context + * @param features zero or more features to check for; {@link com.facebook.widget.FacebookDialog.MessageDialogFeature#MESSAGE_DIALOG} is implicitly + * checked if not explicitly specified + * @return true if all of the specified features are supported by the currently installed version of the + * Facebook application; false if any of the features are not supported + */ + public static boolean canPresentMessageDialog(Context context, MessageDialogFeature... features) { + return handleCanPresent(context, EnumSet.of(MessageDialogFeature.MESSAGE_DIALOG, features)); + } + + /** + * Determines whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features of the native Open Graph action dialog, which in turn may be used to + * determine which UI, etc., to present to the user. + * + * @param context the calling Context + * @param features zero or more features to check for; {@link OpenGraphActionDialogFeature#OG_ACTION_DIALOG} is implicitly + * checked if not explicitly specified + * @return true if all of the specified features are supported by the currently installed version of the + * Facebook application; false if any of the features are not supported + */ + public static boolean canPresentOpenGraphActionDialog(Context context, OpenGraphActionDialogFeature... features) { + return handleCanPresent(context, EnumSet.of(OpenGraphActionDialogFeature.OG_ACTION_DIALOG, features)); + } + + /** + * Determines whether the version of the Facebook application installed on the user's device is recent + * enough to support specific features of the native Open Graph Message dialog, which in turn may be used to + * determine which UI, etc., to present to the user. + * + * @param context the calling Context + * @param features zero or more features to check for; {@link com.facebook.widget.FacebookDialog.OpenGraphMessageDialogFeature#OG_MESSAGE_DIALOG} is + * implicitly checked if not explicitly specified + * @return true if all of the specified features are supported by the currently installed version of the + * Facebook application; false if any of the features are not supported + */ + public static boolean canPresentOpenGraphMessageDialog(Context context, OpenGraphMessageDialogFeature... features) { + return handleCanPresent(context, EnumSet.of(OpenGraphMessageDialogFeature.OG_MESSAGE_DIALOG, features)); + } + + private static boolean handleCanPresent(Context context, Iterable features) { + return getProtocolVersionForNativeDialog(context, getActionForFeatures(features), getMinVersionForFeatures(features)) + != NativeProtocol.NO_PROTOCOL_AVAILABLE; + } + + private static int getProtocolVersionForNativeDialog(Context context, String action, int requiredVersion) { + return NativeProtocol.getLatestAvailableProtocolVersionForAction(context, action, requiredVersion); + } + + private static NativeAppCallAttachmentStore getAttachmentStore() { + if (attachmentStore == null) { + attachmentStore = new NativeAppCallAttachmentStore(); + } + return attachmentStore; + } + + private static int getMinVersionForFeatures(Iterable features) { + int minVersion = Integer.MIN_VALUE; + for (DialogFeature feature : features) { + // Minimum version to support all features is the maximum of each feature's minimum version. + minVersion = Math.max(minVersion, feature.getMinVersion()); + } + return minVersion; + } + + private static String getActionForFeatures(Iterable features) { + String action = null; + for (DialogFeature feature : features) { + // All actions in a set of DialogFeatures should have the same action + // So we can break after assigning the first one + action = feature.getAction(); + break; + } + return action; + } + + private static void logDialogActivity(Activity activity, Fragment fragment, String eventName, String outcome) { + AppEventsLogger logger = AppEventsLogger.newLogger(fragment != null ? fragment.getActivity() : activity); + Bundle parameters = new Bundle(); + parameters.putString(AnalyticsEvents.PARAMETER_DIALOG_OUTCOME, outcome); + logger.logSdkEvent(eventName, null, parameters); + } + + static private String getEventName(Intent intent) { + String action = intent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION); + boolean hasPhotos = intent.hasExtra(NativeProtocol.EXTRA_PHOTOS); + return getEventName(action, hasPhotos); + } + + static private String getEventName(String action, boolean hasPhotos) { + String eventName; + + if (action.equals(NativeProtocol.ACTION_FEED_DIALOG)) { + eventName = hasPhotos ? + AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_PHOTO_SHARE : + AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_SHARE; + } else if (action.equals(NativeProtocol.ACTION_MESSAGE_DIALOG)) { + eventName = hasPhotos ? + AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_PHOTO_MESSAGE : + AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_MESSAGE; + } else if (action.equals(NativeProtocol.ACTION_OGACTIONPUBLISH_DIALOG)) { + eventName = AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_OG_SHARE; + } else if (action.equals(NativeProtocol.ACTION_OGMESSAGEPUBLISH_DIALOG)) { + eventName = AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_OG_MESSAGE; + } else { + throw new FacebookException("An unspecified action was presented"); + } + return eventName; + } + + abstract static class Builder> { + final protected Activity activity; + final protected String applicationId; + final protected PendingCall appCall; + protected Fragment fragment; + protected String applicationName; + protected HashMap imageAttachments = new HashMap(); + protected HashMap imageAttachmentFiles = new HashMap(); + + Builder(Activity activity) { + Validate.notNull(activity, "activity"); + + this.activity = activity; + applicationId = Utility.getMetadataApplicationId(activity); + appCall = new PendingCall(NativeProtocol.DIALOG_REQUEST_CODE); + } + + /** + * Sets the request code that will be passed to handleActivityResult when this activity completes; the + * default is NativeProtocol.DIALOG_REQUEST_CODE. + * + * @param requestCode the request code + * @return this instance of the builder + */ + public CONCRETE setRequestCode(int requestCode) { + this.appCall.setRequestCode(requestCode); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the name of the application to be displayed in the dialog. If provided, this optimizes the user + * experience as a preview of a shared item, etc., can be displayed sooner. + * + * @param applicationName the name of the Facebook application + * @return this instance of the builder + */ + public CONCRETE setApplicationName(String applicationName) { + this.applicationName = applicationName; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the fragment that should launch the dialog. This allows the dialog to be + * launched from a Fragment, and will allow the fragment to receive the + * {@link Fragment#onActivityResult(int, int, android.content.Intent) onActivityResult} + * call rather than the Activity. + * + * @param fragment the fragment that contains this control + */ + public CONCRETE setFragment(Fragment fragment) { + this.fragment = fragment; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Constructs a FacebookDialog with an Intent that is correctly populated to present the dialog within + * the Facebook application. + * + * @return a FacebookDialog instance + */ + public FacebookDialog build() { + validate(); + + Bundle extras = new Bundle(); + putExtra(extras, NativeProtocol.EXTRA_APPLICATION_ID, applicationId); + putExtra(extras, NativeProtocol.EXTRA_APPLICATION_NAME, applicationName); + extras = setBundleExtras(extras); + + String action = getActionForFeatures(getDialogFeatures()); + int protocolVersion = getProtocolVersionForNativeDialog(activity, action, + getMinVersionForFeatures(getDialogFeatures())); + + Intent intent = NativeProtocol.createPlatformActivityIntent(activity, action, protocolVersion, extras); + if (intent == null) { + logDialogActivity(activity, fragment, + getEventName(action, extras.containsKey(NativeProtocol.EXTRA_PHOTOS)), + AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_FAILED); + + throw new FacebookException( + "Unable to create Intent; this likely means the Facebook app is not installed."); + } + appCall.setRequestIntent(intent); + + return new FacebookDialog(activity, fragment, appCall, getOnPresentCallback()); + } + + /** + * Determines whether the native dialog can be presented (i.e., whether the required version of the + * Facebook application is installed on the device, and whether the installed version supports all of + * the parameters specified for the dialog). + * + * @return true if the dialog can be presented; false if not + */ + public boolean canPresent() { + return handleCanPresent(activity, getDialogFeatures()); + } + + void validate() { + } + + OnPresentCallback getOnPresentCallback() { + return new OnPresentCallback() { + @Override + public void onPresent(Context context) throws Exception { + // We're actually being presented, so put our attachments in the content provider. + if (imageAttachments != null && imageAttachments.size() > 0) { + getAttachmentStore().addAttachmentsForCall(context, appCall.getCallId(), imageAttachments); + } + if (imageAttachmentFiles != null && imageAttachmentFiles.size() > 0) { + getAttachmentStore().addAttachmentFilesForCall(context, appCall.getCallId(), + imageAttachmentFiles); + } + } + }; + } + + protected List addImageAttachments(Collection bitmaps) { + ArrayList attachmentUrls = new ArrayList(); + for (Bitmap bitmap : bitmaps) { + String attachmentName = UUID.randomUUID().toString(); + + addImageAttachment(attachmentName, bitmap); + + String url = NativeAppCallContentProvider.getAttachmentUrl(applicationId, appCall.getCallId(), + attachmentName); + attachmentUrls.add(url); + } + + return attachmentUrls; + } + + protected List addImageAttachmentFiles(Collection bitmapFiles) { + ArrayList attachmentUrls = new ArrayList(); + for (File bitmapFile : bitmapFiles) { + String attachmentName = UUID.randomUUID().toString(); + + addImageAttachment(attachmentName, bitmapFile); + + String url = NativeAppCallContentProvider.getAttachmentUrl(applicationId, appCall.getCallId(), + attachmentName); + attachmentUrls.add(url); + } + + return attachmentUrls; + } + + List getImageAttachmentNames() { + return new ArrayList(imageAttachments.keySet()); + } + + abstract Bundle setBundleExtras(Bundle extras); + + void putExtra(Bundle extras, String key, String value) { + if (value != null) { + extras.putString(key, value); + } + } + + abstract EnumSet getDialogFeatures(); + + protected CONCRETE addImageAttachment(String imageName, Bitmap bitmap) { + imageAttachments.put(imageName, bitmap); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + protected CONCRETE addImageAttachment(String imageName, File attachment) { + imageAttachmentFiles.put(imageName, attachment); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + } + + private abstract static class ShareDialogBuilderBase> extends Builder { + private String name; + private String caption; + private String description; + protected String link; + private String picture; + private String place; + private ArrayList friends; + private String ref; + private boolean dataErrorsFatal; + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Share dialog; must not be null + */ + public ShareDialogBuilderBase(Activity activity) { + super(activity); + } + + /** + * Sets the title of the item to be shared. + * + * @param name the title + * @return this instance of the builder + */ + public CONCRETE setName(String name) { + this.name = name; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the subtitle of the item to be shared. + * + * @param caption the subtitle + * @return this instance of the builder + */ + public CONCRETE setCaption(String caption) { + this.caption = caption; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the description of the item to be shared. + * + * @param description the description + * @return this instance of the builder + */ + public CONCRETE setDescription(String description) { + this.description = description; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the URL of the item to be shared. + * + * @param link the URL + * @return this instance of the builder + */ + public CONCRETE setLink(String link) { + this.link = link; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the URL of the image of the item to be shared. + * + * @param picture the URL of the image + * @return this instance of the builder + */ + public CONCRETE setPicture(String picture) { + this.picture = picture; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the place for the item to be shared. + * + * @param place the Facebook ID of the place + * @return this instance of the builder + */ + public CONCRETE setPlace(String place) { + this.place = place; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the tagged friends for the item to be shared. + * + * @param friends a list of Facebook IDs of the friends to be tagged in the shared item + * @return this instance of the builder + */ + public CONCRETE setFriends(List friends) { + this.friends = new ArrayList(friends); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the 'ref' property of the item to be shared. + * + * @param ref the 'ref' property + * @return this instance of the builder + */ + public CONCRETE setRef(String ref) { + this.ref = ref; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets whether errors encountered during previewing the shared item should be considered fatal and + * cause the dialog to return an error + * + * @param dataErrorsFatal true if data errors should be fatal; false if not + * @return this instance of the builder + */ + public CONCRETE setDataErrorsFatal(boolean dataErrorsFatal) { + this.dataErrorsFatal = dataErrorsFatal; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + @Override + Bundle setBundleExtras(Bundle extras) { + putExtra(extras, NativeProtocol.EXTRA_APPLICATION_ID, applicationId); + putExtra(extras, NativeProtocol.EXTRA_APPLICATION_NAME, applicationName); + putExtra(extras, NativeProtocol.EXTRA_TITLE, name); + putExtra(extras, NativeProtocol.EXTRA_SUBTITLE, caption); + putExtra(extras, NativeProtocol.EXTRA_DESCRIPTION, description); + putExtra(extras, NativeProtocol.EXTRA_LINK, link); + putExtra(extras, NativeProtocol.EXTRA_IMAGE, picture); + putExtra(extras, NativeProtocol.EXTRA_PLACE_TAG, place); + putExtra(extras, NativeProtocol.EXTRA_TITLE, name); + putExtra(extras, NativeProtocol.EXTRA_REF, ref); + + extras.putBoolean(NativeProtocol.EXTRA_DATA_FAILURES_FATAL, dataErrorsFatal); + if (!Utility.isNullOrEmpty(friends)) { + extras.putStringArrayList(NativeProtocol.EXTRA_FRIEND_TAGS, friends); + } + return extras; + } + } + + /** + * Provides a builder which can construct a FacebookDialog instance suitable for presenting the native + * Share dialog. This builder will throw an exception if the Facebook application is not installed, so it + * should only be used if {@link FacebookDialog#canPresentShareDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.ShareDialogFeature...)} indicates the capability is available. + */ + public static class ShareDialogBuilder extends ShareDialogBuilderBase { + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Share dialog; must not be null + */ + public ShareDialogBuilder(Activity activity) { + super(activity); + } + + @Override + EnumSet getDialogFeatures() { + return EnumSet.of(ShareDialogFeature.SHARE_DIALOG); + } + } + + private static abstract class PhotoDialogBuilderBase> + extends Builder { + static int MAXIMUM_PHOTO_COUNT = 6; + private String place; + private ArrayList friends; + private ArrayList imageAttachmentUrls = new ArrayList(); + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Share dialog; must not be null + */ + public PhotoDialogBuilderBase(Activity activity) { + super(activity); + } + + /** + * Sets the place for the item to be shared. + * + * @param place the Facebook ID of the place + * @return this instance of the builder + */ + public CONCRETE setPlace(String place) { + this.place = place; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the tagged friends for the item to be shared. + * + * @param friends a list of Facebook IDs of the friends to be tagged in the shared item + * @return this instance of the builder + */ + public CONCRETE setFriends(List friends) { + this.friends = new ArrayList(friends); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + *

Adds one or more photos to the list of photos to display in the native Share dialog, by providing + * an in-memory representation of the photos. The dialog's callback will be called once the user has + * shared the photos, but the photos themselves may be uploaded in the background by the Facebook app; + * apps wishing to be notified when the photo upload has succeeded or failed should extend the + * FacebookBroadcastReceiver class and register it in their AndroidManifest.xml.

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * No more than six photos may be shared at a time. + * @param photos a collection of Files representing photos to be uploaded + * @return this instance of the builder + */ + public CONCRETE addPhotos(Collection photos) { + imageAttachmentUrls.addAll(addImageAttachments(photos)); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Adds one or more photos to the list of photos to display in the native Share dialog, by specifying + * their location in the file system. The dialog's callback will be called once the user has + * shared the photos, but the photos themselves may be uploaded in the background by the Facebook app; + * apps wishing to be notified when the photo upload has succeeded or failed should extend the + * FacebookBroadcastReceiver class and register it in their AndroidManifest.xml. + * No more than six photos may be shared at a time. + * @param photos a collection of Files representing photos to be uploaded + * @return this instance of the builder + */ + public CONCRETE addPhotoFiles(Collection photos) { + imageAttachmentUrls.addAll(addImageAttachmentFiles(photos)); + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + abstract int getMaximumNumberOfPhotos(); + + @Override + void validate() { + super.validate(); + + if (imageAttachmentUrls.isEmpty()) { + throw new FacebookException("Must specify at least one photo."); + } + + if (imageAttachmentUrls.size() > getMaximumNumberOfPhotos()) { + throw new FacebookException(String.format("Cannot add more than %d photos.", getMaximumNumberOfPhotos())); + } + } + + @Override + Bundle setBundleExtras(Bundle extras) { + putExtra(extras, NativeProtocol.EXTRA_APPLICATION_ID, applicationId); + putExtra(extras, NativeProtocol.EXTRA_APPLICATION_NAME, applicationName); + putExtra(extras, NativeProtocol.EXTRA_PLACE_TAG, place); + extras.putStringArrayList(NativeProtocol.EXTRA_PHOTOS, imageAttachmentUrls); + + if (!Utility.isNullOrEmpty(friends)) { + extras.putStringArrayList(NativeProtocol.EXTRA_FRIEND_TAGS, friends); + } + return extras; + } + } + + /** + * Provides a builder which can construct a FacebookDialog instance suitable for presenting the native + * Share dialog for sharing photos. This builder will throw an exception if the Facebook application is not + * installed, so it should only be used if {@link FacebookDialog#canPresentShareDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.ShareDialogFeature...)} indicates the capability is available. + */ + public static class PhotoShareDialogBuilder extends PhotoDialogBuilderBase { + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Share dialog; must not be null + */ + public PhotoShareDialogBuilder(Activity activity) { + super(activity); + } + + @Override + EnumSet getDialogFeatures() { + return EnumSet.of(ShareDialogFeature.SHARE_DIALOG, ShareDialogFeature.PHOTOS); + } + + @Override + int getMaximumNumberOfPhotos() { + return MAXIMUM_PHOTO_COUNT; + } + } + + /** + * Provides a builder which can construct a FacebookDialog instance suitable for presenting the native + * Message dialog for sharing photos. This builder will throw an exception if the Messenger application is not + * installed, so it should only be used if {@link FacebookDialog#canPresentMessageDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.MessageDialogFeature...)} indicates the capability is available. + */ + public static class PhotoMessageDialogBuilder extends PhotoDialogBuilderBase { + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Message dialog; must not be null + */ + public PhotoMessageDialogBuilder(Activity activity) { + super(activity); + } + + @Override + EnumSet getDialogFeatures() { + return EnumSet.of(MessageDialogFeature.MESSAGE_DIALOG, MessageDialogFeature.PHOTOS); + } + + @Override + int getMaximumNumberOfPhotos() { + return MAXIMUM_PHOTO_COUNT; + } + } + + /** + * Provides a builder which can construct a FacebookDialog instance suitable for presenting the native + * Message dialog. This builder will throw an exception if the Facebook Messenger application is not installed, so it + * should only be used if {@link FacebookDialog#canPresentMessageDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.MessageDialogFeature...)} indicates the capability is available. + * The "friends" and "place" properties will be ignored as the Facebook Messenger app does not support tagging. + */ + public static class MessageDialogBuilder extends ShareDialogBuilderBase { + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Message dialog; must not be null + */ + public MessageDialogBuilder(Activity activity) { + super(activity); + } + + @Override + EnumSet getDialogFeatures() { + return EnumSet.of(MessageDialogFeature.MESSAGE_DIALOG); + } + } + + private static abstract class OpenGraphDialogBuilderBase> + extends Builder { + + private String previewPropertyName; + private OpenGraphAction action; + private String actionType; + private boolean dataErrorsFatal; + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Open Graph action publish dialog; + * must not be null + * @param action the Open Graph action to be published, which must contain a reference to at least one + * Open Graph object with the property name specified by setPreviewPropertyName; the action + * must have had its type specified via the {@link OpenGraphAction#setType(String)} method + * @param actionType the type of the Open Graph action to be published, which should be the namespace-qualified + * name of the action type (e.g., "myappnamespace:myactiontype"); this will override the type + * of the action passed in. + * @param previewPropertyName the name of a property on the Open Graph action that contains the + * Open Graph object which will be displayed as a preview to the user + */ + @Deprecated + public OpenGraphDialogBuilderBase(Activity activity, OpenGraphAction action, String actionType, + String previewPropertyName) { + super(activity); + + Validate.notNull(action, "action"); + Validate.notNullOrEmpty(actionType, "actionType"); + Validate.notNullOrEmpty(previewPropertyName, "previewPropertyName"); + if (action.getProperty(previewPropertyName) == null) { + throw new IllegalArgumentException( + "A property named \"" + previewPropertyName + "\" was not found on the action. The name of " + + "the preview property must match the name of an action property."); + } + String typeOnAction = action.getType(); + if (!Utility.isNullOrEmpty(typeOnAction) && !typeOnAction.equals(actionType)) { + throw new IllegalArgumentException("'actionType' must match the type of 'action' if it is specified. " + + "Consider using OpenGraphDialogBuilderBase(Activity activity, OpenGraphAction action, " + + "String previewPropertyName) instead."); + } + this.action = action; + this.actionType = actionType; + this.previewPropertyName = previewPropertyName; + } + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Open Graph action publish dialog; + * must not be null + * @param action the Open Graph action to be published, which must contain a reference to at least one + * Open Graph object with the property name specified by setPreviewPropertyName; the action + * must have had its type specified via the {@link OpenGraphAction#setType(String)} method + * @param previewPropertyName the name of a property on the Open Graph action that contains the + * Open Graph object which will be displayed as a preview to the user + */ + public OpenGraphDialogBuilderBase(Activity activity, OpenGraphAction action, String previewPropertyName) { + super(activity); + + Validate.notNull(action, "action"); + Validate.notNullOrEmpty(action.getType(), "action.getType()"); + Validate.notNullOrEmpty(previewPropertyName, "previewPropertyName"); + if (action.getProperty(previewPropertyName) == null) { + throw new IllegalArgumentException( + "A property named \"" + previewPropertyName + "\" was not found on the action. The name of " + + "the preview property must match the name of an action property."); + } + + this.action = action; + this.actionType = action.getType(); + this.previewPropertyName = previewPropertyName; + } + + /** + * Sets whether errors encountered during previewing the shared item should be considered fatal and + * cause the dialog to return an error + * + * @param dataErrorsFatal true if data errors should be fatal; false if not + * @return this instance of the builder + */ + public CONCRETE setDataErrorsFatal(boolean dataErrorsFatal) { + this.dataErrorsFatal = dataErrorsFatal; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + *

Specifies a list of images for the Open Graph action that should be uploaded prior to publishing the + * action. The action must already have been set prior to calling this method. This method will generate unique + * names for the image attachments and update the action to refer to these attachments. Note that calling + * setAction again after calling this method will not clear the image attachments already set, but the new + * action will have no reference to the existing attachments. The images will not be marked as being + * user-generated.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param bitmaps a list of Bitmaps to be uploaded and attached to the Open Graph action + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentsForAction(List bitmaps) { + return setImageAttachmentsForAction(bitmaps, false); + } + + /** + *

Specifies a list of images for the Open Graph action that should be uploaded prior to publishing the + * action. The action must already have been set prior to calling this method. This method will generate unique + * names for the image attachments and update the action to refer to these attachments. Note that calling + * setAction again after calling this method will not clear the image attachments already set, but the new + * action will have no reference to the existing attachments. The images may be marked as being + * user-generated -- refer to + * this article + * for more information.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param bitmaps a list of Bitmaps to be uploaded and attached to the Open Graph action + * @param isUserGenerated if true, specifies that the user_generated flag should be set for these images + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentsForAction(List bitmaps, + boolean isUserGenerated) { + Validate.containsNoNulls(bitmaps, "bitmaps"); + if (action == null) { + throw new FacebookException("Can not set attachments prior to setting action."); + } + + List attachmentUrls = addImageAttachments(bitmaps); + updateActionAttachmentUrls(attachmentUrls, isUserGenerated); + + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + *

Specifies a list of images for the Open Graph action that should be uploaded prior to publishing the + * action. The action must already have been set prior to calling this method. The images will not be marked + * as being user-generated. This method will generate unique names for the image attachments and update the + * action to refer to these attachments. Note that calling setAction again after calling this method will + * not clear the image attachments already set, but the new action will have no reference to the existing + * attachments.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param bitmapFiles a list of Files containing bitmaps to be uploaded and attached to the Open Graph action + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentFilesForAction(List bitmapFiles) { + return setImageAttachmentFilesForAction(bitmapFiles, false); + } + + /** + *

Specifies a list of images for the Open Graph action that should be uploaded prior to publishing the + * action. The action must already have been set prior to calling this method. The images may be marked as being + * user-generated -- refer to + * this article + * for more information. This method will generate unique + * names for the image attachments and update the action to refer to these attachments. Note that calling + * setAction again after calling this method will not clear the image attachments already set, but the new + * action will have no reference to the existing attachments.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param bitmapFiles a list of Files containing bitmaps to be uploaded and attached to the Open Graph action + * @param isUserGenerated if true, specifies that the user_generated flag should be set for these images + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentFilesForAction(List bitmapFiles, + boolean isUserGenerated) { + Validate.containsNoNulls(bitmapFiles, "bitmapFiles"); + if (action == null) { + throw new FacebookException("Can not set attachments prior to setting action."); + } + + List attachmentUrls = addImageAttachmentFiles(bitmapFiles); + updateActionAttachmentUrls(attachmentUrls, isUserGenerated); + + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + private void updateActionAttachmentUrls(List attachmentUrls, boolean isUserGenerated) { + List attachments = action.getImage(); + if (attachments == null) { + attachments = new ArrayList(attachmentUrls.size()); + } + + for (String url : attachmentUrls) { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put(NativeProtocol.IMAGE_URL_KEY, url); + if (isUserGenerated) { + jsonObject.put(NativeProtocol.IMAGE_USER_GENERATED_KEY, true); + } + } catch (JSONException e) { + throw new FacebookException("Unable to attach images", e); + } + attachments.add(jsonObject); + } + action.setImage(attachments); + } + + + /** + *

Specifies a list of images for an Open Graph object referenced by the action that should be uploaded + * prior to publishing the action. The images will not be marked as user-generated. + * The action must already have been set prior to calling this method, and + * the action must have a GraphObject-valued property with the specified property name. This method will + * generate unique names for the image attachments and update the graph object to refer to these + * attachments. Note that calling setObject again after calling this method, or modifying the value of the + * specified property, will not clear the image attachments already set, but the new action (or objects) + * will have no reference to the existing attachments.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param objectProperty the name of a property on the action that corresponds to an Open Graph object; + * the object must be marked as a new object to be created + * (i.e., {@link com.facebook.model.OpenGraphObject#getCreateObject()} must return + * true) or an exception will be thrown + * @param bitmaps a list of Files containing bitmaps to be uploaded and attached to the Open Graph object + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentsForObject(String objectProperty, List bitmaps) { + return setImageAttachmentsForObject(objectProperty, bitmaps, false); + } + + /** + *

Specifies a list of images for an Open Graph object referenced by the action that should be uploaded + * prior to publishing the action. The images may be marked as being + * user-generated -- refer to + * this article + * for more information. + * The action must already have been set prior to calling this method, and + * the action must have a GraphObject-valued property with the specified property name. This method will + * generate unique names for the image attachments and update the graph object to refer to these + * attachments. Note that calling setObject again after calling this method, or modifying the value of the + * specified property, will not clear the image attachments already set, but the new action (or objects) + * will have no reference to the existing attachments.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param objectProperty the name of a property on the action that corresponds to an Open Graph object; + * the object must be marked as a new object to be created + * (i.e., {@link com.facebook.model.OpenGraphObject#getCreateObject()} must return + * true) or an exception will be thrown + * @param objectProperty the name of a property on the action that corresponds to an Open Graph object + * @param bitmaps a list of Files containing bitmaps to be uploaded and attached to the Open Graph object + * @param isUserGenerated if true, specifies that the user_generated flag should be set for these images + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentsForObject(String objectProperty, List bitmaps, + boolean isUserGenerated) { + Validate.notNull(objectProperty, "objectProperty"); + Validate.containsNoNulls(bitmaps, "bitmaps"); + if (action == null) { + throw new FacebookException("Can not set attachments prior to setting action."); + } + + List attachmentUrls = addImageAttachments(bitmaps); + updateObjectAttachmentUrls(objectProperty, attachmentUrls, isUserGenerated); + + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + *

Specifies a list of images for an Open Graph object referenced by the action that should be uploaded + * prior to publishing the action. The images will not be marked as user-generated. + * The action must already have been set prior to calling this method, and + * the action must have a GraphObject-valued property with the specified property name. This method will + * generate unique names for the image attachments and update the graph object to refer to these + * attachments. Note that calling setObject again after calling this method, or modifying the value of the + * specified property, will not clear the image attachments already set, but the new action (or objects) + * will have no reference to the existing attachments.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param objectProperty the name of a property on the action that corresponds to an Open Graph object; + * the object must be marked as a new object to be created + * (i.e., {@link com.facebook.model.OpenGraphObject#getCreateObject()} must return + * true) or an exception will be thrown + * @param bitmapFiles a list of Bitmaps to be uploaded and attached to the Open Graph object + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentFilesForObject(String objectProperty, + List bitmapFiles) { + return setImageAttachmentFilesForObject(objectProperty, bitmapFiles, false); + } + + /** + *

Specifies a list of images for an Open Graph object referenced by the action that should be uploaded + * prior to publishing the action. The images may be marked as being + * user-generated -- refer to + * this article + * for more information. + * The action must already have been set prior to calling this method, and + * the action must have a GraphObject-valued property with the specified property name. This method will + * generate unique names for the image attachments and update the graph object to refer to these + * attachments. Note that calling setObject again after calling this method, or modifying the value of the + * specified property, will not clear the image attachments already set, but the new action (or objects) + * will have no reference to the existing attachments.

+ *

+ *

In order for the images to be provided to the Facebook application as part of the app call, the + * NativeAppCallContentProvider must be specified correctly in the application's AndroidManifest.xml.

+ * + * @param objectProperty the name of a property on the action that corresponds to an Open Graph object; + * the object must be marked as a new object to be created + * (i.e., {@link com.facebook.model.OpenGraphObject#getCreateObject()} must return + * true) or an exception will be thrown + * @param bitmapFiles a list of Bitmaps to be uploaded and attached to the Open Graph object + * @param isUserGenerated if true, specifies that the user_generated flag should be set for these images + * @return this instance of the builder + */ + public CONCRETE setImageAttachmentFilesForObject(String objectProperty, + List bitmapFiles, boolean isUserGenerated) { + Validate.notNull(objectProperty, "objectProperty"); + Validate.containsNoNulls(bitmapFiles, "bitmapFiles"); + if (action == null) { + throw new FacebookException("Can not set attachments prior to setting action."); + } + + List attachmentUrls = addImageAttachmentFiles(bitmapFiles); + updateObjectAttachmentUrls(objectProperty, attachmentUrls, isUserGenerated); + + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + void updateObjectAttachmentUrls(String objectProperty, List attachmentUrls, boolean isUserGenerated) { + final OpenGraphObject object; + try { + object = action.getPropertyAs(objectProperty, OpenGraphObject.class); + if (object == null) { + throw new IllegalArgumentException("Action does not contain a property '" + objectProperty + "'"); + } + } catch (FacebookGraphObjectException exception) { + throw new IllegalArgumentException("Property '" + objectProperty + "' is not a graph object"); + } + if (!object.getCreateObject()) { + throw new IllegalArgumentException( + "The Open Graph object in '" + objectProperty + "' is not marked for creation"); + } + + GraphObjectList attachments = object.getImage(); + if (attachments == null) { + attachments = GraphObject.Factory.createList(GraphObject.class); + } + for (String url : attachmentUrls) { + GraphObject graphObject = GraphObject.Factory.create(); + graphObject.setProperty(NativeProtocol.IMAGE_URL_KEY, url); + if (isUserGenerated) { + graphObject.setProperty(NativeProtocol.IMAGE_USER_GENERATED_KEY, true); + } + attachments.add(graphObject); + } + object.setImage(attachments); + } + + @Override + Bundle setBundleExtras(Bundle extras) { + putExtra(extras, NativeProtocol.EXTRA_PREVIEW_PROPERTY_NAME, previewPropertyName); + putExtra(extras, NativeProtocol.EXTRA_ACTION_TYPE, actionType); + extras.putBoolean(NativeProtocol.EXTRA_DATA_FAILURES_FATAL, dataErrorsFatal); + + JSONObject jsonAction = action.getInnerJSONObject(); + jsonAction = flattenChildrenOfGraphObject(jsonAction); + + String jsonString = jsonAction.toString(); + putExtra(extras, NativeProtocol.EXTRA_ACTION, jsonString); + + return extras; + } + + private JSONObject flattenChildrenOfGraphObject(JSONObject graphObject) { + try { + // Clone the existing object to avoid modifying it from under the caller. + graphObject = new JSONObject(graphObject.toString()); + + @SuppressWarnings("unchecked") + Iterator keys = graphObject.keys(); + while (keys.hasNext()) { + String key = keys.next(); + // The "image" property should not be flattened + if (!key.equalsIgnoreCase("image")) { + Object object = graphObject.get(key); + + object = flattenObject(object); + graphObject.put(key, object); + } + } + + return graphObject; + } catch (JSONException e) { + throw new FacebookException(e); + } + } + + private Object flattenObject(Object object) throws JSONException { + if (object == null) { + return null; + } + + if (object instanceof JSONObject) { + JSONObject jsonObject = (JSONObject) object; + + // Don't flatten objects that are marked as create_object. + if (jsonObject.optBoolean(NativeProtocol.OPEN_GRAPH_CREATE_OBJECT_KEY)) { + return object; + } + if (jsonObject.has("id")) { + return jsonObject.getString("id"); + } else if (jsonObject.has("url")) { + return jsonObject.getString("url"); + } + } else if (object instanceof JSONArray) { + JSONArray jsonArray = (JSONArray) object; + JSONArray newArray = new JSONArray(); + int length = jsonArray.length(); + + for (int i = 0; i < length; ++i) { + newArray.put(flattenObject(jsonArray.get(i))); + } + + return newArray; + } + + return object; + } + } + + /** + * Provides a builder which can construct a FacebookDialog instance suitable for presenting the native + * Open Graph action publish dialog. This builder allows the caller to specify binary images for both the + * action and any Open Graph objects to be created prior to publishing the action. + * This builder will throw an exception if the Facebook application is not installed, so it + * should only be used if {@link FacebookDialog#canPresentOpenGraphActionDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.OpenGraphActionDialogFeature...)} indicates the capability is available. + */ + public static class OpenGraphActionDialogBuilder extends OpenGraphDialogBuilderBase { + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Open Graph action publish dialog; + * must not be null + * @param action the Open Graph action to be published, which must contain a reference to at least one + * Open Graph object with the property name specified by setPreviewPropertyName; the action + * must have had its type specified via the {@link OpenGraphAction#setType(String)} method + * @param actionType the type of the Open Graph action to be published, which should be the namespace-qualified + * name of the action type (e.g., "myappnamespace:myactiontype"); this will override the type + * of the action passed in. + * @param previewPropertyName the name of a property on the Open Graph action that contains the + * Open Graph object which will be displayed as a preview to the user + */ + @Deprecated + public OpenGraphActionDialogBuilder(Activity activity, OpenGraphAction action, String actionType, + String previewPropertyName) { + super(activity, action, actionType, previewPropertyName); + } + + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Open Graph action publish dialog; + * must not be null + * @param action the Open Graph action to be published, which must contain a reference to at least one + * Open Graph object with the property name specified by setPreviewPropertyName; the action + * must have had its type specified via the {@link OpenGraphAction#setType(String)} method + * @param previewPropertyName the name of a property on the Open Graph action that contains the + * Open Graph object which will be displayed as a preview to the user + */ + public OpenGraphActionDialogBuilder(Activity activity, OpenGraphAction action, String previewPropertyName) { + super(activity, action, previewPropertyName); + } + + @Override + EnumSet getDialogFeatures() { + return EnumSet.of(OpenGraphActionDialogFeature.OG_ACTION_DIALOG); + } + } + + /** + * Provides a builder which can construct a FacebookDialog instance suitable for presenting the native + * Open Graph action message dialog. This builder allows the caller to specify binary images for both the + * action and any Open Graph objects to be created prior to publishing the action. + * This builder will throw an exception if the Facebook application is not installed, so it + * should only be used if {@link FacebookDialog#canPresentOpenGraphMessageDialog(android.content.Context, + * com.facebook.widget.FacebookDialog.OpenGraphMessageDialogFeature...)} indicates the capability is available. + */ + public static class OpenGraphMessageDialogBuilder extends OpenGraphDialogBuilderBase { + /** + * Constructor. + * + * @param activity the Activity which is presenting the native Open Graph action message dialog; + * must not be null + * @param action the Open Graph action to be sent, which must contain a reference to at least one + * Open Graph object with the property name specified by setPreviewPropertyName; the action + * must have had its type specified via the {@link OpenGraphAction#setType(String)} method + * @param previewPropertyName the name of a property on the Open Graph action that contains the + * Open Graph object which will be displayed as a preview to the user + */ + public OpenGraphMessageDialogBuilder(Activity activity, OpenGraphAction action, String previewPropertyName) { + super(activity, action, previewPropertyName); + } + + @Override + EnumSet getDialogFeatures() { + return EnumSet.of(OpenGraphMessageDialogFeature.OG_MESSAGE_DIALOG); + } + } + + /** + * Encapsulates information about a call being made to the Facebook application for Android. A unique String + * call ID is used to track calls through their lifecycle. + */ + public static class PendingCall implements Parcelable { + private UUID callId; + private Intent requestIntent; + private int requestCode; + + /** + * Constructor. + * + * @param requestCode the request code for this app call + */ + public PendingCall(int requestCode) { + callId = UUID.randomUUID(); + this.requestCode = requestCode; + } + + private PendingCall(Parcel in) { + callId = UUID.fromString(in.readString()); + requestIntent = in.readParcelable(null); + requestCode = in.readInt(); + } + + private void setRequestIntent(Intent requestIntent) { + this.requestIntent = requestIntent; + this.requestIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID, callId.toString()); + } + + /** + * Returns the Intent that was used to initiate this call to the Facebook application. + * + * @return the Intent + */ + public Intent getRequestIntent() { + return requestIntent; + } + + /** + * Returns the unique ID of this call to the Facebook application. + * + * @return the unique ID + */ + public UUID getCallId() { + return callId; + } + + private void setRequestCode(int requestCode) { + this.requestCode = requestCode; + } + + /** + * Gets the request code for this call. + * + * @return the request code that will be passed to handleActivityResult upon completion. + */ + public int getRequestCode() { + return requestCode; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(callId.toString()); + parcel.writeParcelable(requestIntent, 0); + parcel.writeInt(requestCode); + } + + public static final Creator CREATOR + = new Creator() { + public PendingCall createFromParcel(Parcel in) { + return new PendingCall(in); + } + + public PendingCall[] newArray(int size) { + return new PendingCall[size]; + } + }; + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FacebookFragment.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FacebookFragment.java new file mode 100644 index 0000000000..8c7f8dbb21 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FacebookFragment.java @@ -0,0 +1,297 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import com.facebook.Session; +import com.facebook.SessionLoginBehavior; +import com.facebook.SessionState; +import com.facebook.internal.SessionAuthorizationType; +import com.facebook.internal.SessionTracker; + +import java.util.Date; +import java.util.List; + +/** + *

Basic implementation of a Fragment that uses a Session to perform + * Single Sign On (SSO). This class is package private, and is not intended + * to be consumed by external applications.

+ * + *

The method {@link android.support.v4.app.Fragment#onActivityResult} is + * used to manage the session information, so if you override it in a subclass, + * be sure to call {@code super.onActivityResult}.

+ * + *

The methods in this class are not thread-safe.

+ */ +public class FacebookFragment extends Fragment { + + private SessionTracker sessionTracker; + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + sessionTracker = new SessionTracker(getActivity(), new DefaultSessionStatusCallback()); + } + + /** + * Called when the activity that was launched exits. This method manages session + * information when a session is opened. If this method is overridden in subclasses, + * be sure to call {@code super.onActivityResult(...)} first. + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + sessionTracker.getSession().onActivityResult(this.getActivity(), requestCode, resultCode, data); + } + + @Override + public void onDestroy() { + super.onDestroy(); + sessionTracker.stopTracking(); + } + + /** + * Use the supplied Session object instead of the active Session. + * + * @param newSession the Session object to use + */ + public void setSession(Session newSession) { + if (sessionTracker != null) { + sessionTracker.setSession(newSession); + } + } + + // METHOD TO BE OVERRIDDEN + + /** + * Called when the session state changes. Override this method to take action + * on session state changes. + * + * @param state the new state + * @param exception any exceptions that occurred during the state change + */ + protected void onSessionStateChange(SessionState state, Exception exception) { + } + + // ACCESSORS (CANNOT BE OVERRIDDEN) + + /** + * Gets the current Session. + * + * @return the current Session object. + */ + protected final Session getSession() { + if (sessionTracker != null) { + return sessionTracker.getSession(); + } + return null; + } + + /** + * Determines whether the current session is open. + * + * @return true if the current session is open + */ + protected final boolean isSessionOpen() { + if (sessionTracker != null) { + return sessionTracker.getOpenSession() != null; + } + return false; + } + + /** + * Gets the current state of the session or null if no session has been created. + * + * @return the current state of the session + */ + protected final SessionState getSessionState() { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getSession(); + return (currentSession != null) ? currentSession.getState() : null; + } + return null; + } + + /** + * Gets the access token associated with the current session or null if no + * session has been created. + * + * @return the access token + */ + protected final String getAccessToken() { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getOpenSession(); + return (currentSession != null) ? currentSession.getAccessToken() : null; + } + return null; + } + + /** + * Gets the date at which the current session will expire or null if no session + * has been created. + * + * @return the date at which the current session will expire + */ + protected final Date getExpirationDate() { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getOpenSession(); + return (currentSession != null) ? currentSession.getExpirationDate() : null; + } + return null; + } + + /** + * Closes the current session. + */ + protected final void closeSession() { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getOpenSession(); + if (currentSession != null) { + currentSession.close(); + } + } + } + + /** + * Closes the current session as well as clearing the token cache. + */ + protected final void closeSessionAndClearTokenInformation() { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getOpenSession(); + if (currentSession != null) { + currentSession.closeAndClearTokenInformation(); + } + } + } + + /** + * Gets the permissions associated with the current session or null if no session + * has been created. + * + * @return the permissions associated with the current session + */ + protected final List getSessionPermissions() { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getSession(); + return (currentSession != null) ? currentSession.getPermissions() : null; + } + return null; + } + + /** + * Opens a new session. This method will use the application id from + * the associated meta-data value and an empty list of permissions. + */ + protected final void openSession() { + openSessionForRead(null, null); + } + + /** + * Opens a new session with read permissions. If either applicationID or permissions + * is null, this method will default to using the values from the associated + * meta-data value and an empty list respectively. + * + * @param applicationId the applicationID, can be null + * @param permissions the permissions list, can be null + */ + protected final void openSessionForRead(String applicationId, List permissions) { + openSessionForRead(applicationId, permissions, SessionLoginBehavior.SSO_WITH_FALLBACK, + Session.DEFAULT_AUTHORIZE_ACTIVITY_CODE); + } + + /** + * Opens a new session with read permissions. If either applicationID or permissions + * is null, this method will default to using the values from the associated + * meta-data value and an empty list respectively. + * + * @param applicationId the applicationID, can be null + * @param permissions the permissions list, can be null + * @param behavior the login behavior to use with the session + * @param activityCode the activity code to use for the SSO activity + */ + protected final void openSessionForRead(String applicationId, List permissions, + SessionLoginBehavior behavior, int activityCode) { + openSession(applicationId, permissions, behavior, activityCode, SessionAuthorizationType.READ); + } + + /** + * Opens a new session with publish permissions. If either applicationID is null, + * this method will default to using the value from the associated + * meta-data value. The permissions list cannot be null. + * + * @param applicationId the applicationID, can be null + * @param permissions the permissions list, cannot be null + */ + protected final void openSessionForPublish(String applicationId, List permissions) { + openSessionForPublish(applicationId, permissions, SessionLoginBehavior.SSO_WITH_FALLBACK, + Session.DEFAULT_AUTHORIZE_ACTIVITY_CODE); + } + + /** + * Opens a new session with publish permissions. If either applicationID is null, + * this method will default to using the value from the associated + * meta-data value. The permissions list cannot be null. + * + * @param applicationId the applicationID, can be null + * @param permissions the permissions list, cannot be null + * @param behavior the login behavior to use with the session + * @param activityCode the activity code to use for the SSO activity + */ + protected final void openSessionForPublish(String applicationId, List permissions, + SessionLoginBehavior behavior, int activityCode) { + openSession(applicationId, permissions, behavior, activityCode, SessionAuthorizationType.PUBLISH); + } + + private void openSession(String applicationId, List permissions, + SessionLoginBehavior behavior, int activityCode, SessionAuthorizationType authType) { + if (sessionTracker != null) { + Session currentSession = sessionTracker.getSession(); + if (currentSession == null || currentSession.getState().isClosed()) { + Session session = new Session.Builder(getActivity()).setApplicationId(applicationId).build(); + Session.setActiveSession(session); + currentSession = session; + } + if (!currentSession.isOpened()) { + Session.OpenRequest openRequest = new Session.OpenRequest(this). + setPermissions(permissions). + setLoginBehavior(behavior). + setRequestCode(activityCode); + if (SessionAuthorizationType.PUBLISH.equals(authType)) { + currentSession.openForPublish(openRequest); + } else { + currentSession.openForRead(openRequest); + } + } + } + } + + /** + * The default callback implementation for the session. + */ + private class DefaultSessionStatusCallback implements Session.StatusCallback { + + @Override + public void call(Session session, + SessionState state, + Exception exception) { + FacebookFragment.this.onSessionStateChange(state, exception); + } + + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FriendPickerFragment.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FriendPickerFragment.java new file mode 100644 index 0000000000..b657ec567f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/FriendPickerFragment.java @@ -0,0 +1,318 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.AttributeSet; +import com.facebook.AppEventsLogger; +import com.facebook.FacebookException; +import com.facebook.Request; +import com.facebook.Session; +import com.facebook.android.R; +import com.facebook.internal.AnalyticsEvents; +import com.facebook.model.GraphUser; + +import java.util.*; + +/** + * Provides a Fragment that displays a list of a user's friends and allows one or more of the + * friends to be selected. + */ +public class FriendPickerFragment extends PickerFragment { + /** + * The key for a String parameter in the fragment's Intent bundle to indicate what user's + * friends should be shown. The default is to display the currently authenticated user's friends. + */ + public static final String USER_ID_BUNDLE_KEY = "com.facebook.widget.FriendPickerFragment.UserId"; + /** + * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the + * picker should allow more than one friend to be selected or not. + */ + public static final String MULTI_SELECT_BUNDLE_KEY = "com.facebook.widget.FriendPickerFragment.MultiSelect"; + + private static final String ID = "id"; + private static final String NAME = "name"; + + private String userId; + + private boolean multiSelect = true; + + private List preSelectedFriendIds = new ArrayList(); + + /** + * Default constructor. Creates a Fragment with all default properties. + */ + public FriendPickerFragment() { + this(null); + } + + /** + * Constructor. + * @param args a Bundle that optionally contains one or more values containing additional + * configuration information for the Fragment. + */ + @SuppressLint("ValidFragment") + public FriendPickerFragment(Bundle args) { + super(GraphUser.class, R.layout.com_facebook_friendpickerfragment, args); + setFriendPickerSettingsFromBundle(args); + } + + /** + * Gets the ID of the user whose friends should be displayed. If null, the default is to + * show the currently authenticated user's friends. + * @return the user ID, or null + */ + public String getUserId() { + return userId; + } + + /** + * Sets the ID of the user whose friends should be displayed. If null, the default is to + * show the currently authenticated user's friends. + * @param userId the user ID, or null + */ + public void setUserId(String userId) { + this.userId = userId; + } + + /** + * Gets whether the user can select multiple friends, or only one friend. + * @return true if the user can select multiple friends, false if only one friend + */ + public boolean getMultiSelect() { + return multiSelect; + } + + /** + * Sets whether the user can select multiple friends, or only one friend. + * @param multiSelect true if the user can select multiple friends, false if only one friend + */ + public void setMultiSelect(boolean multiSelect) { + if (this.multiSelect != multiSelect) { + this.multiSelect = multiSelect; + setSelectionStrategy(createSelectionStrategy()); + } + } + + /** + * Sets the list of friends for pre selection. These friends will be selected by default. + * @param userIds list of friends as ids + */ + public void setSelectionByIds(List userIds) { + preSelectedFriendIds.addAll(userIds); + } + + /** + * Sets the list of friends for pre selection. These friends will be selected by default. + * @param userIds list of friends as ids + */ + public void setSelectionByIds(String... userIds) { + setSelectionByIds(Arrays.asList(userIds)); + } + + /** + * Sets the list of friends for pre selection. These friends will be selected by default. + * @param graphUsers list of friends as GraphUsers + */ + public void setSelection(GraphUser... graphUsers) { + setSelection(Arrays.asList(graphUsers)); + } + + /** + * Sets the list of friends for pre selection. These friends will be selected by default. + * @param graphUsers list of friends as GraphUsers + */ + public void setSelection(List graphUsers) { + List userIds = new ArrayList(); + for(GraphUser graphUser: graphUsers) { + userIds.add(graphUser.getId()); + } + setSelectionByIds(userIds); + } + + /** + * Gets the currently-selected list of users. + * @return the currently-selected list of users + */ + public List getSelection() { + return getSelectedGraphObjects(); + } + + @Override + public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { + super.onInflate(activity, attrs, savedInstanceState); + TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_friend_picker_fragment); + + setMultiSelect(a.getBoolean(R.styleable.com_facebook_friend_picker_fragment_multi_select, multiSelect)); + + a.recycle(); + } + + public void setSettingsFromBundle(Bundle inState) { + super.setSettingsFromBundle(inState); + setFriendPickerSettingsFromBundle(inState); + } + + void saveSettingsToBundle(Bundle outState) { + super.saveSettingsToBundle(outState); + + outState.putString(USER_ID_BUNDLE_KEY, userId); + outState.putBoolean(MULTI_SELECT_BUNDLE_KEY, multiSelect); + } + + @Override + PickerFragmentAdapter createAdapter() { + PickerFragmentAdapter adapter = new PickerFragmentAdapter( + this.getActivity()) { + + @Override + protected int getGraphObjectRowLayoutId(GraphUser graphObject) { + return R.layout.com_facebook_picker_list_row; + } + + @Override + protected int getDefaultPicture() { + return R.drawable.com_facebook_profile_default_icon; + } + + }; + adapter.setShowCheckbox(true); + adapter.setShowPicture(getShowPictures()); + adapter.setSortFields(Arrays.asList(new String[]{NAME})); + adapter.setGroupByField(NAME); + + return adapter; + } + + @Override + LoadingStrategy createLoadingStrategy() { + return new ImmediateLoadingStrategy(); + } + + @Override + SelectionStrategy createSelectionStrategy() { + return multiSelect ? new MultiSelectionStrategy() : new SingleSelectionStrategy(); + } + + @Override + Request getRequestForLoadData(Session session) { + if (adapter == null) { + throw new FacebookException("Can't issue requests until Fragment has been created."); + } + + String userToFetch = (userId != null) ? userId : "me"; + return createRequest(userToFetch, extraFields, session); + } + + @Override + String getDefaultTitleText() { + return getString(R.string.com_facebook_choose_friends); + } + + @Override + void logAppEvents(boolean doneButtonClicked) { + AppEventsLogger logger = AppEventsLogger.newLogger(this.getActivity(), getSession()); + Bundle parameters = new Bundle(); + + // If Done was clicked, we know this completed successfully. If not, we don't know (caller might have + // dismissed us in response to selection changing, or user might have hit back button). Either way + // we'll log the number of selections. + String outcome = doneButtonClicked ? AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_COMPLETED : + AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_UNKNOWN; + parameters.putString(AnalyticsEvents.PARAMETER_DIALOG_OUTCOME, outcome); + parameters.putInt("num_friends_picked", getSelection().size()); + + logger.logSdkEvent(AnalyticsEvents.EVENT_FRIEND_PICKER_USAGE, null, parameters); + } + + @Override + public void loadData(boolean forceReload) { + super.loadData(forceReload); + setSelectedGraphObjects(preSelectedFriendIds); + } + + private Request createRequest(String userID, Set extraFields, Session session) { + Request request = Request.newGraphPathRequest(session, userID + "/friends", null); + + Set fields = new HashSet(extraFields); + String[] requiredFields = new String[]{ + ID, + NAME + }; + fields.addAll(Arrays.asList(requiredFields)); + + String pictureField = adapter.getPictureFieldSpecifier(); + if (pictureField != null) { + fields.add(pictureField); + } + + Bundle parameters = request.getParameters(); + parameters.putString("fields", TextUtils.join(",", fields)); + request.setParameters(parameters); + + return request; + } + + private void setFriendPickerSettingsFromBundle(Bundle inState) { + // We do this in a separate non-overridable method so it is safe to call from the constructor. + if (inState != null) { + if (inState.containsKey(USER_ID_BUNDLE_KEY)) { + setUserId(inState.getString(USER_ID_BUNDLE_KEY)); + } + setMultiSelect(inState.getBoolean(MULTI_SELECT_BUNDLE_KEY, multiSelect)); + } + } + + private class ImmediateLoadingStrategy extends LoadingStrategy { + @Override + protected void onLoadFinished(GraphObjectPagingLoader loader, + SimpleGraphObjectCursor data) { + super.onLoadFinished(loader, data); + + // We could be called in this state if we are clearing data or if we are being re-attached + // in the middle of a query. + if (data == null || loader.isLoading()) { + return; + } + + if (data.areMoreObjectsAvailable()) { + // We got results, but more are available. + followNextLink(); + } else { + // We finished loading results. + hideActivityCircle(); + + // If this was from the cache, schedule a delayed refresh query (unless we got no results + // at all, in which case refresh immediately. + if (data.isFromCache()) { + loader.refreshOriginalRequest(data.getCount() == 0 ? CACHED_RESULT_REFRESH_DELAY : 0); + } + } + } + + private void followNextLink() { + // This may look redundant, but this causes the circle to be alpha-dimmed if we have results. + displayActivityCircle(); + + loader.followNextLink(); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectAdapter.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectAdapter.java new file mode 100644 index 0000000000..552474933f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectAdapter.java @@ -0,0 +1,827 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.content.Context; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.*; +import com.facebook.*; +import com.facebook.android.R; +import com.facebook.internal.ImageDownloader; +import com.facebook.internal.ImageRequest; +import com.facebook.internal.ImageResponse; +import com.facebook.model.GraphObject; +import org.json.JSONObject; + +import java.net.URI; +import java.net.URISyntaxException; +import java.text.Collator; +import java.util.*; + +class GraphObjectAdapter extends BaseAdapter implements SectionIndexer { + private static final int DISPLAY_SECTIONS_THRESHOLD = 1; + private static final int HEADER_VIEW_TYPE = 0; + private static final int GRAPH_OBJECT_VIEW_TYPE = 1; + private static final int ACTIVITY_CIRCLE_VIEW_TYPE = 2; + private static final int MAX_PREFETCHED_PICTURES = 20; + + private static final String ID = "id"; + private static final String NAME = "name"; + private static final String PICTURE = "picture"; + + private final Map pendingRequests = new HashMap(); + private final LayoutInflater inflater; + private List sectionKeys = new ArrayList(); + private Map> graphObjectsBySection = new HashMap>(); + private Map graphObjectsById = new HashMap(); + private boolean displaySections; + private List sortFields; + private String groupByField; + private boolean showPicture; + private boolean showCheckbox; + private Filter filter; + private DataNeededListener dataNeededListener; + private GraphObjectCursor cursor; + private Context context; + private Map prefetchedPictureCache = new HashMap(); + private ArrayList prefetchedProfilePictureIds = new ArrayList(); + private OnErrorListener onErrorListener; + + public interface DataNeededListener { + public void onDataNeeded(); + } + + public interface OnErrorListener { + void onError(GraphObjectAdapter adapter, FacebookException error); + } + + public static class SectionAndItem { + public String sectionKey; + public T graphObject; + + public enum Type { + GRAPH_OBJECT, + SECTION_HEADER, + ACTIVITY_CIRCLE + } + + public SectionAndItem(String sectionKey, T graphObject) { + this.sectionKey = sectionKey; + this.graphObject = graphObject; + } + + public Type getType() { + if (sectionKey == null) { + return Type.ACTIVITY_CIRCLE; + } else if (graphObject == null) { + return Type.SECTION_HEADER; + } else { + return Type.GRAPH_OBJECT; + } + } + } + + interface Filter { + boolean includeItem(T graphObject); + } + + public GraphObjectAdapter(Context context) { + this.context = context; + this.inflater = LayoutInflater.from(context); + } + + public List getSortFields() { + return sortFields; + } + + public void setSortFields(List sortFields) { + this.sortFields = sortFields; + } + + public String getGroupByField() { + return groupByField; + } + + public void setGroupByField(String groupByField) { + this.groupByField = groupByField; + } + + public boolean getShowPicture() { + return showPicture; + } + + public void setShowPicture(boolean showPicture) { + this.showPicture = showPicture; + } + + public boolean getShowCheckbox() { + return showCheckbox; + } + + public void setShowCheckbox(boolean showCheckbox) { + this.showCheckbox = showCheckbox; + } + + public DataNeededListener getDataNeededListener() { + return dataNeededListener; + } + + public void setDataNeededListener(DataNeededListener dataNeededListener) { + this.dataNeededListener = dataNeededListener; + } + + public OnErrorListener getOnErrorListener() { + return onErrorListener; + } + + public void setOnErrorListener(OnErrorListener onErrorListener) { + this.onErrorListener = onErrorListener; + } + + public GraphObjectCursor getCursor() { + return cursor; + } + + public boolean changeCursor(GraphObjectCursor cursor) { + if (this.cursor == cursor) { + return false; + } + if (this.cursor != null) { + this.cursor.close(); + } + this.cursor = cursor; + + rebuildAndNotify(); + return true; + } + + public void rebuildAndNotify() { + rebuildSections(); + notifyDataSetChanged(); + } + + public void prioritizeViewRange(int firstVisibleItem, int lastVisibleItem, int prefetchBuffer) { + if ((lastVisibleItem < firstVisibleItem) || (sectionKeys.size() == 0)) { + return; + } + + // We want to prioritize requests for items which are visible but do not have pictures + // loaded yet. We also want to pre-fetch pictures for items which are not yet visible + // but are within a buffer on either side of the visible items, on the assumption that + // they will be visible soon. For these latter items, we'll store the images in memory + // in the hopes we can immediately populate their image view when needed. + + // Prioritize the requests in reverse order since each call to prioritizeRequest will just + // move it to the front of the queue. And we want the earliest ones in the range to be at + // the front of the queue, so all else being equal, the list will appear to populate from + // the top down. + for (int i = lastVisibleItem; i >= 0; i--) { + SectionAndItem sectionAndItem = getSectionAndItem(i); + if (sectionAndItem.graphObject != null) { + String id = getIdOfGraphObject(sectionAndItem.graphObject); + ImageRequest request = pendingRequests.get(id); + if (request != null) { + ImageDownloader.prioritizeRequest(request); + } + } + } + + // For items which are not visible, but within the buffer on either side, we want to + // fetch those items and store them in a small in-memory cache of bitmaps. + int start = Math.max(0, firstVisibleItem - prefetchBuffer); + int end = Math.min(lastVisibleItem + prefetchBuffer, getCount() - 1); + ArrayList graphObjectsToPrefetchPicturesFor = new ArrayList(); + // Add the IDs before and after the visible range. + for (int i = start; i < firstVisibleItem; ++i) { + SectionAndItem sectionAndItem = getSectionAndItem(i); + if (sectionAndItem.graphObject != null) { + graphObjectsToPrefetchPicturesFor.add(sectionAndItem.graphObject); + } + } + for (int i = lastVisibleItem + 1; i <= end; ++i) { + SectionAndItem sectionAndItem = getSectionAndItem(i); + if (sectionAndItem.graphObject != null) { + graphObjectsToPrefetchPicturesFor.add(sectionAndItem.graphObject); + } + } + for (T graphObject : graphObjectsToPrefetchPicturesFor) { + URI uri = getPictureUriOfGraphObject(graphObject); + final String id = getIdOfGraphObject(graphObject); + + // This URL already have been requested for pre-fetching, but we want to act in an LRU manner, so move + // it to the end of the list regardless. + boolean alreadyPrefetching = prefetchedProfilePictureIds.remove(id); + prefetchedProfilePictureIds.add(id); + + // If we've already requested it for pre-fetching, no need to do so again. + if (!alreadyPrefetching) { + downloadProfilePicture(id, uri, null); + } + } + } + + protected String getSectionKeyOfGraphObject(T graphObject) { + String result = null; + + if (groupByField != null) { + result = (String) graphObject.getProperty(groupByField); + if (result != null && result.length() > 0) { + result = result.substring(0, 1).toUpperCase(); + } + } + + return (result != null) ? result : ""; + } + + protected CharSequence getTitleOfGraphObject(T graphObject) { + return (String) graphObject.getProperty(NAME); + } + + protected CharSequence getSubTitleOfGraphObject(T graphObject) { + return null; + } + + protected URI getPictureUriOfGraphObject(T graphObject) { + String uri = null; + Object o = graphObject.getProperty(PICTURE); + if (o instanceof String) { + uri = (String) o; + } else if (o instanceof JSONObject) { + ItemPicture itemPicture = GraphObject.Factory.create((JSONObject) o).cast(ItemPicture.class); + ItemPictureData data = itemPicture.getData(); + if (data != null) { + uri = data.getUrl(); + } + } + + if (uri != null) { + try { + return new URI(uri); + } catch (URISyntaxException e) { + } + } + return null; + } + + protected View getSectionHeaderView(String sectionHeader, View convertView, ViewGroup parent) { + TextView result = (TextView) convertView; + + if (result == null) { + result = (TextView) inflater.inflate(R.layout.com_facebook_picker_list_section_header, null); + } + + result.setText(sectionHeader); + + return result; + } + + protected View getGraphObjectView(T graphObject, View convertView, ViewGroup parent) { + View result = convertView; + + if (result == null) { + result = createGraphObjectView(graphObject); + } + + populateGraphObjectView(result, graphObject); + return result; + } + + private View getActivityCircleView(View convertView, ViewGroup parent) { + View result = convertView; + + if (result == null) { + result = inflater.inflate(R.layout.com_facebook_picker_activity_circle_row, null); + } + ProgressBar activityCircle = (ProgressBar) result.findViewById(R.id.com_facebook_picker_row_activity_circle); + activityCircle.setVisibility(View.VISIBLE); + + return result; + } + + protected int getGraphObjectRowLayoutId(T graphObject) { + return R.layout.com_facebook_picker_list_row; + } + + protected int getDefaultPicture() { + return R.drawable.com_facebook_profile_default_icon; + } + + protected View createGraphObjectView(T graphObject) { + View result = inflater.inflate(getGraphObjectRowLayoutId(graphObject), null); + + ViewStub checkboxStub = (ViewStub) result.findViewById(R.id.com_facebook_picker_checkbox_stub); + if (checkboxStub != null) { + if (!getShowCheckbox()) { + checkboxStub.setVisibility(View.GONE); + } else { + CheckBox checkBox = (CheckBox) checkboxStub.inflate(); + updateCheckboxState(checkBox, false); + } + } + + ViewStub profilePicStub = (ViewStub) result.findViewById(R.id.com_facebook_picker_profile_pic_stub); + if (!getShowPicture()) { + profilePicStub.setVisibility(View.GONE); + } else { + ImageView imageView = (ImageView) profilePicStub.inflate(); + imageView.setVisibility(View.VISIBLE); + } + + return result; + } + + protected void populateGraphObjectView(View view, T graphObject) { + String id = getIdOfGraphObject(graphObject); + view.setTag(id); + + CharSequence title = getTitleOfGraphObject(graphObject); + TextView titleView = (TextView) view.findViewById(R.id.com_facebook_picker_title); + if (titleView != null) { + titleView.setText(title, TextView.BufferType.SPANNABLE); + } + + CharSequence subtitle = getSubTitleOfGraphObject(graphObject); + TextView subtitleView = (TextView) view.findViewById(R.id.picker_subtitle); + if (subtitleView != null) { + if (subtitle != null) { + subtitleView.setText(subtitle, TextView.BufferType.SPANNABLE); + subtitleView.setVisibility(View.VISIBLE); + } else { + subtitleView.setVisibility(View.GONE); + } + } + + if (getShowCheckbox()) { + CheckBox checkBox = (CheckBox) view.findViewById(R.id.com_facebook_picker_checkbox); + updateCheckboxState(checkBox, isGraphObjectSelected(id)); + } + + if (getShowPicture()) { + URI pictureURI = getPictureUriOfGraphObject(graphObject); + + if (pictureURI != null) { + ImageView profilePic = (ImageView) view.findViewById(R.id.com_facebook_picker_image); + + // See if we have already pre-fetched this; if not, download it. + if (prefetchedPictureCache.containsKey(id)) { + ImageResponse response = prefetchedPictureCache.get(id); + profilePic.setImageBitmap(response.getBitmap()); + profilePic.setTag(response.getRequest().getImageUri()); + } else { + downloadProfilePicture(id, pictureURI, profilePic); + } + } + } + } + + /** + * @throws FacebookException if the GraphObject doesn't have an ID. + */ + String getIdOfGraphObject(T graphObject) { + if (graphObject.asMap().containsKey(ID)) { + Object obj = graphObject.getProperty(ID); + if (obj instanceof String) { + return (String) obj; + } + } + throw new FacebookException("Received an object without an ID."); + } + + boolean filterIncludesItem(T graphObject) { + return filter == null || filter.includeItem(graphObject); + } + + Filter getFilter() { + return filter; + } + + void setFilter(Filter filter) { + this.filter = filter; + } + + boolean isGraphObjectSelected(String graphObjectId) { + return false; + } + + void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) { + // Default is no-op + } + + String getPictureFieldSpecifier() { + // How big is our image? + View view = createGraphObjectView(null); + ImageView picture = (ImageView) view.findViewById(R.id.com_facebook_picker_image); + if (picture == null) { + return null; + } + + // Note: these dimensions are in pixels, not dips + ViewGroup.LayoutParams layoutParams = picture.getLayoutParams(); + return String.format(Locale.US, "picture.height(%d).width(%d)", layoutParams.height, layoutParams.width); + } + + + private boolean shouldShowActivityCircleCell() { + // We show the "more data" activity circle cell if we have a listener to request more data, + // we are expecting more data, and we have some data already (i.e., not on a fresh query). + return (cursor != null) && cursor.areMoreObjectsAvailable() && (dataNeededListener != null) && !isEmpty(); + } + + private void rebuildSections() { + sectionKeys = new ArrayList(); + graphObjectsBySection = new HashMap>(); + graphObjectsById = new HashMap(); + displaySections = false; + + if (cursor == null || cursor.getCount() == 0) { + return; + } + + int objectsAdded = 0; + cursor.moveToFirst(); + do { + T graphObject = cursor.getGraphObject(); + + if (!filterIncludesItem(graphObject)) { + continue; + } + + objectsAdded++; + + String sectionKeyOfItem = getSectionKeyOfGraphObject(graphObject); + if (!graphObjectsBySection.containsKey(sectionKeyOfItem)) { + sectionKeys.add(sectionKeyOfItem); + graphObjectsBySection.put(sectionKeyOfItem, new ArrayList()); + } + List section = graphObjectsBySection.get(sectionKeyOfItem); + section.add(graphObject); + + graphObjectsById.put(getIdOfGraphObject(graphObject), graphObject); + } while (cursor.moveToNext()); + + if (sortFields != null) { + final Collator collator = Collator.getInstance(); + for (List section : graphObjectsBySection.values()) { + Collections.sort(section, new Comparator() { + @Override + public int compare(GraphObject a, GraphObject b) { + return compareGraphObjects(a, b, sortFields, collator); + } + }); + } + } + + Collections.sort(sectionKeys, Collator.getInstance()); + + displaySections = sectionKeys.size() > 1 && objectsAdded > DISPLAY_SECTIONS_THRESHOLD; + } + + SectionAndItem getSectionAndItem(int position) { + if (sectionKeys.size() == 0) { + return null; + } + String sectionKey = null; + T graphObject = null; + + if (!displaySections) { + sectionKey = sectionKeys.get(0); + List section = graphObjectsBySection.get(sectionKey); + if (position >= 0 && position < section.size()) { + graphObject = graphObjectsBySection.get(sectionKey).get(position); + } else { + // We are off the end; we must be adding an activity circle to indicate more data is coming. + assert dataNeededListener != null && cursor.areMoreObjectsAvailable(); + // We return null for both to indicate this. + return new SectionAndItem(null, null); + } + } else { + // Count through the sections; the "0" position in each section is the header. We decrement + // position each time we skip forward a certain number of elements, including the header. + for (String key : sectionKeys) { + // Decrement if we skip over the header + if (position-- == 0) { + sectionKey = key; + break; + } + + List section = graphObjectsBySection.get(key); + if (position < section.size()) { + // The position is somewhere in this section. Get the corresponding graph object. + sectionKey = key; + graphObject = section.get(position); + break; + } + // Decrement by as many items as we skipped over + position -= section.size(); + } + } + if (sectionKey != null) { + // Note: graphObject will be null if this represents a section header. + return new SectionAndItem(sectionKey, graphObject); + } else { + throw new IndexOutOfBoundsException("position"); + } + } + + int getPosition(String sectionKey, T graphObject) { + int position = 0; + boolean found = false; + + // First find the section key and increment position one for each header we will render; + // increment by the size of each section prior to the one we want. + for (String key : sectionKeys) { + if (displaySections) { + position++; + } + if (key.equals(sectionKey)) { + found = true; + break; + } else { + position += graphObjectsBySection.get(key).size(); + } + } + + if (!found) { + return -1; + } else if (graphObject == null) { + // null represents the header for a section; we counted this header in position earlier, + // so subtract it back out. + return position - (displaySections ? 1 : 0); + } + + // Now find index of this item within that section. + for (T t : graphObjectsBySection.get(sectionKey)) { + if (GraphObject.Factory.hasSameId(t, graphObject)) { + return position; + } + position++; + } + return -1; + } + + @Override + public boolean isEmpty() { + // We'll never populate sectionKeys unless we have at least one object. + return sectionKeys.size() == 0; + } + + @Override + public int getCount() { + if (sectionKeys.size() == 0) { + return 0; + } + + // If we are not displaying sections, we don't display a header; otherwise, we have one header per item in + // addition to the actual items. + int count = (displaySections) ? sectionKeys.size() : 0; + for (List section : graphObjectsBySection.values()) { + count += section.size(); + } + + // If we should show a cell with an activity circle indicating more data is coming, add it to the count. + if (shouldShowActivityCircleCell()) { + ++count; + } + + return count; + } + + @Override + public boolean areAllItemsEnabled() { + return displaySections; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public boolean isEnabled(int position) { + SectionAndItem sectionAndItem = getSectionAndItem(position); + return sectionAndItem.getType() == SectionAndItem.Type.GRAPH_OBJECT; + } + + @Override + public Object getItem(int position) { + SectionAndItem sectionAndItem = getSectionAndItem(position); + return (sectionAndItem.getType() == SectionAndItem.Type.GRAPH_OBJECT) ? sectionAndItem.graphObject : null; + } + + @Override + public long getItemId(int position) { + // We assume IDs that can be converted to longs. If this is not the case for certain types of + // GraphObjects, subclasses should override this to return, e.g., position, and override hasStableIds + // to return false. + SectionAndItem sectionAndItem = getSectionAndItem(position); + if (sectionAndItem != null && sectionAndItem.graphObject != null) { + String id = getIdOfGraphObject(sectionAndItem.graphObject); + if (id != null) { + return Long.parseLong(id); + } + } + return 0; + } + + @Override + public int getViewTypeCount() { + return 3; + } + + @Override + public int getItemViewType(int position) { + SectionAndItem sectionAndItem = getSectionAndItem(position); + switch (sectionAndItem.getType()) { + case SECTION_HEADER: + return HEADER_VIEW_TYPE; + case GRAPH_OBJECT: + return GRAPH_OBJECT_VIEW_TYPE; + case ACTIVITY_CIRCLE: + return ACTIVITY_CIRCLE_VIEW_TYPE; + default: + throw new FacebookException("Unexpected type of section and item."); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SectionAndItem sectionAndItem = getSectionAndItem(position); + + switch (sectionAndItem.getType()) { + case SECTION_HEADER: + return getSectionHeaderView(sectionAndItem.sectionKey, convertView, parent); + case GRAPH_OBJECT: + return getGraphObjectView(sectionAndItem.graphObject, convertView, parent); + case ACTIVITY_CIRCLE: + // If we get a request for this view, it means we need more data. + assert cursor.areMoreObjectsAvailable() && (dataNeededListener != null); + dataNeededListener.onDataNeeded(); + return getActivityCircleView(convertView, parent); + default: + throw new FacebookException("Unexpected type of section and item."); + } + } + + @Override + public Object[] getSections() { + if (displaySections) { + return sectionKeys.toArray(); + } else { + return new Object[0]; + } + } + + @Override + public int getPositionForSection(int section) { + if (displaySections) { + section = Math.max(0, Math.min(section, sectionKeys.size() - 1)); + if (section < sectionKeys.size()) { + return getPosition(sectionKeys.get(section), null); + } + } + return 0; + } + + @Override + public int getSectionForPosition(int position) { + SectionAndItem sectionAndItem = getSectionAndItem(position); + if (sectionAndItem != null && + sectionAndItem.getType() != SectionAndItem.Type.ACTIVITY_CIRCLE) { + return Math.max(0, Math.min(sectionKeys.indexOf(sectionAndItem.sectionKey), sectionKeys.size() - 1)); + } + return 0; + } + + public List getGraphObjectsById(Collection ids) { + Set idSet = new HashSet(); + idSet.addAll(ids); + + ArrayList result = new ArrayList(idSet.size()); + for (String id : idSet) { + T graphObject = graphObjectsById.get(id); + if (graphObject != null) { + result.add(graphObject); + } + } + + return result; + } + + private void downloadProfilePicture(final String profileId, URI pictureURI, final ImageView imageView) { + if (pictureURI == null) { + return; + } + + // If we don't have an imageView, we are pre-fetching this image to store in-memory because we + // think the user might scroll to its corresponding list row. If we do have an imageView, we + // only want to queue a download if the view's tag isn't already set to the URL (which would mean + // it's already got the correct picture). + boolean prefetching = imageView == null; + if (prefetching || !pictureURI.equals(imageView.getTag())) { + if (!prefetching) { + // Setting the tag to the profile ID indicates that we're currently downloading the + // picture for this profile; we'll set it to the actual picture URL when complete. + imageView.setTag(profileId); + imageView.setImageResource(getDefaultPicture()); + } + + ImageRequest.Builder builder = new ImageRequest.Builder(context.getApplicationContext(), pictureURI) + .setCallerTag(this) + .setCallback( + new ImageRequest.Callback() { + @Override + public void onCompleted(ImageResponse response) { + processImageResponse(response, profileId, imageView); + } + }); + + ImageRequest newRequest = builder.build(); + pendingRequests.put(profileId, newRequest); + + ImageDownloader.downloadAsync(newRequest); + } + } + + private void callOnErrorListener(Exception exception) { + if (onErrorListener != null) { + if (!(exception instanceof FacebookException)) { + exception = new FacebookException(exception); + } + onErrorListener.onError(this, (FacebookException) exception); + } + } + + private void processImageResponse(ImageResponse response, String graphObjectId, ImageView imageView) { + pendingRequests.remove(graphObjectId); + if (response.getError() != null) { + callOnErrorListener(response.getError()); + } + + if (imageView == null) { + // This was a pre-fetch request. + if (response.getBitmap() != null) { + // Is the cache too big? + if (prefetchedPictureCache.size() >= MAX_PREFETCHED_PICTURES) { + // Find the oldest one and remove it. + String oldestId = prefetchedProfilePictureIds.remove(0); + prefetchedPictureCache.remove(oldestId); + } + prefetchedPictureCache.put(graphObjectId, response); + } + } else if (graphObjectId.equals(imageView.getTag())) { + Exception error = response.getError(); + Bitmap bitmap = response.getBitmap(); + if (error == null && bitmap != null) { + imageView.setImageBitmap(bitmap); + imageView.setTag(response.getRequest().getImageUri()); + } + } + } + + private static int compareGraphObjects(GraphObject a, GraphObject b, Collection sortFields, + Collator collator) { + for (String sortField : sortFields) { + String sa = (String) a.getProperty(sortField); + String sb = (String) b.getProperty(sortField); + + if (sa != null && sb != null) { + int result = collator.compare(sa, sb); + if (result != 0) { + return result; + } + } else if (!(sa == null && sb == null)) { + return (sa == null) ? -1 : 1; + } + } + return 0; + } + + + // Graph object type to navigate the JSON that sometimes comes back instead of a URL string + private interface ItemPicture extends GraphObject { + ItemPictureData getData(); + } + + // Graph object type to navigate the JSON that sometimes comes back instead of a URL string + private interface ItemPictureData extends GraphObject { + String getUrl(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectCursor.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectCursor.java new file mode 100644 index 0000000000..9495535e0f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectCursor.java @@ -0,0 +1,55 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import com.facebook.model.GraphObject; + +interface GraphObjectCursor { + boolean isFromCache(); + + boolean areMoreObjectsAvailable(); + + int getCount(); + + int getPosition(); + + boolean move(int offset); + + boolean moveToPosition(int position); + + boolean moveToFirst(); + + boolean moveToLast(); + + boolean moveToNext(); + + boolean moveToPrevious(); + + boolean isFirst(); + + boolean isLast(); + + boolean isBeforeFirst(); + + boolean isAfterLast(); + + T getGraphObject(); + + void close(); + + boolean isClosed(); +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectPagingLoader.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectPagingLoader.java new file mode 100644 index 0000000000..26b33c99dc --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/GraphObjectPagingLoader.java @@ -0,0 +1,226 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.content.Context; +import android.os.Handler; +import android.support.v4.content.Loader; +import com.facebook.*; +import com.facebook.model.GraphObject; +import com.facebook.model.GraphObjectList; +import com.facebook.internal.CacheableRequestBatch; + +class GraphObjectPagingLoader extends Loader> { + private final Class graphObjectClass; + private boolean skipRoundtripIfCached; + private Request originalRequest; + private Request currentRequest; + private Request nextRequest; + private OnErrorListener onErrorListener; + private SimpleGraphObjectCursor cursor; + private boolean appendResults = false; + private boolean loading = false; + + public interface OnErrorListener { + public void onError(FacebookException error, GraphObjectPagingLoader loader); + } + + public GraphObjectPagingLoader(Context context, Class graphObjectClass) { + super(context); + + this.graphObjectClass = graphObjectClass; + } + + public OnErrorListener getOnErrorListener() { + return onErrorListener; + } + + public void setOnErrorListener(OnErrorListener listener) { + this.onErrorListener = listener; + } + + public SimpleGraphObjectCursor getCursor() { + return cursor; + } + + public void clearResults() { + nextRequest = null; + originalRequest = null; + currentRequest = null; + + deliverResult(null); + } + + public boolean isLoading() { + return loading; + } + + public void startLoading(Request request, boolean skipRoundtripIfCached) { + originalRequest = request; + startLoading(request, skipRoundtripIfCached, 0); + } + + public void refreshOriginalRequest(long afterDelay) { + if (originalRequest == null) { + throw new FacebookException( + "refreshOriginalRequest may not be called until after startLoading has been called."); + } + startLoading(originalRequest, false, afterDelay); + } + + public void followNextLink() { + if (nextRequest != null) { + appendResults = true; + currentRequest = nextRequest; + + currentRequest.setCallback(new Request.Callback() { + @Override + public void onCompleted(Response response) { + requestCompleted(response); + } + }); + + loading = true; + CacheableRequestBatch batch = putRequestIntoBatch(currentRequest, skipRoundtripIfCached); + Request.executeBatchAsync(batch); + } + } + + @Override + public void deliverResult(SimpleGraphObjectCursor cursor) { + SimpleGraphObjectCursor oldCursor = this.cursor; + this.cursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + + if (cursor != null) { + deliverResult(cursor); + } + } + + private void startLoading(Request request, boolean skipRoundtripIfCached, long afterDelay) { + this.skipRoundtripIfCached = skipRoundtripIfCached; + appendResults = false; + nextRequest = null; + currentRequest = request; + currentRequest.setCallback(new Request.Callback() { + @Override + public void onCompleted(Response response) { + requestCompleted(response); + } + }); + + // We are considered loading even if we have a delay. + loading = true; + + final RequestBatch batch = putRequestIntoBatch(request, skipRoundtripIfCached); + Runnable r = new Runnable() { + @Override + public void run() { + Request.executeBatchAsync(batch); + } + }; + if (afterDelay == 0) { + r.run(); + } else { + Handler handler = new Handler(); + handler.postDelayed(r, afterDelay); + } + } + + private CacheableRequestBatch putRequestIntoBatch(Request request, boolean skipRoundtripIfCached) { + // We just use the request URL as the cache key. + CacheableRequestBatch batch = new CacheableRequestBatch(request); + // We use the default cache key (request URL). + batch.setForceRoundTrip(!skipRoundtripIfCached); + return batch; + } + + private void requestCompleted(Response response) { + Request request = response.getRequest(); + if (request != currentRequest) { + return; + } + + loading = false; + currentRequest = null; + + FacebookRequestError requestError = response.getError(); + FacebookException exception = (requestError == null) ? null : requestError.getException(); + if (response.getGraphObject() == null && exception == null) { + exception = new FacebookException("GraphObjectPagingLoader received neither a result nor an error."); + } + + if (exception != null) { + nextRequest = null; + + if (onErrorListener != null) { + onErrorListener.onError(exception, this); + } + } else { + addResults(response); + } + } + + private void addResults(Response response) { + SimpleGraphObjectCursor cursorToModify = (cursor == null || !appendResults) ? new SimpleGraphObjectCursor() : + new SimpleGraphObjectCursor(cursor); + + PagedResults result = response.getGraphObjectAs(PagedResults.class); + boolean fromCache = response.getIsFromCache(); + + GraphObjectList data = result.getData().castToListOf(graphObjectClass); + boolean haveData = data.size() > 0; + + if (haveData) { + nextRequest = response.getRequestForPagedResults(Response.PagingDirection.NEXT); + + cursorToModify.addGraphObjects(data, fromCache); + cursorToModify.setMoreObjectsAvailable(true); + } + + if (!haveData) { + cursorToModify.setMoreObjectsAvailable(false); + cursorToModify.setFromCache(fromCache); + + nextRequest = null; + } + + // Once we get any set of results NOT from the cache, stop trying to get any future ones + // from it. + if (!fromCache) { + skipRoundtripIfCached = false; + } + + deliverResult(cursorToModify); + } + + interface PagedResults extends GraphObject { + GraphObjectList getData(); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/LoginButton.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/LoginButton.java new file mode 100644 index 0000000000..828b21d303 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/LoginButton.java @@ -0,0 +1,900 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Typeface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; + +import com.facebook.*; +import com.facebook.android.R; +import com.facebook.internal.AnalyticsEvents; +import com.facebook.model.GraphUser; +import com.facebook.internal.SessionAuthorizationType; +import com.facebook.internal.SessionTracker; +import com.facebook.internal.Utility; +import com.facebook.internal.Utility.FetchedAppSettings; + +import java.util.*; + +/** + * A Log In/Log Out button that maintains session state and logs + * in/out for the app. + *

+ * This control will create and use the active session upon construction + * if it has the available data (if the app ID is specified in the manifest). + * It will also open the active session if it does not require user interaction + * (i.e. if the session is in the {@link com.facebook.SessionState#CREATED_TOKEN_LOADED} state. + * Developers can override the use of the active session by calling + * the {@link #setSession(com.facebook.Session)} method. + */ +public class LoginButton extends Button { + + public static enum ToolTipMode { + /** + * Default display mode. A server query will determine if the tool tip should be displayed + * and, if so, what the string shown to the user should be. + */ + DEFAULT, + + /** + * Display the tool tip with a local string--regardless of what the server returns + */ + DISPLAY_ALWAYS, + + /** + * Never display the tool tip--regardless of what the server says + */ + NEVER_DISPLAY + } + + private static final String TAG = LoginButton.class.getName(); + private String applicationId = null; + private SessionTracker sessionTracker; + private GraphUser user = null; + private Session userInfoSession = null; // the Session used to fetch the current user info + private boolean confirmLogout; + private boolean fetchUserInfo; + private String loginText; + private String logoutText; + private UserInfoChangedCallback userInfoChangedCallback; + private Fragment parentFragment; + private LoginButtonProperties properties = new LoginButtonProperties(); + private String loginLogoutEventName = AnalyticsEvents.EVENT_LOGIN_VIEW_USAGE; + private OnClickListener listenerCallback; + private boolean nuxChecked; + private ToolTipPopup.Style nuxStyle = ToolTipPopup.Style.BLUE; + private ToolTipMode nuxMode = ToolTipMode.DEFAULT; + private long nuxDisplayTime = ToolTipPopup.DEFAULT_POPUP_DISPLAY_TIME; + private ToolTipPopup nuxPopup; + + static class LoginButtonProperties { + private SessionDefaultAudience defaultAudience = SessionDefaultAudience.FRIENDS; + private List permissions = Collections.emptyList(); + private SessionAuthorizationType authorizationType = null; + private OnErrorListener onErrorListener; + private SessionLoginBehavior loginBehavior = SessionLoginBehavior.SSO_WITH_FALLBACK; + private Session.StatusCallback sessionStatusCallback; + + public void setOnErrorListener(OnErrorListener onErrorListener) { + this.onErrorListener = onErrorListener; + } + + public OnErrorListener getOnErrorListener() { + return onErrorListener; + } + + public void setDefaultAudience(SessionDefaultAudience defaultAudience) { + this.defaultAudience = defaultAudience; + } + + public SessionDefaultAudience getDefaultAudience() { + return defaultAudience; + } + + public void setReadPermissions(List permissions, Session session) { + if (SessionAuthorizationType.PUBLISH.equals(authorizationType)) { + throw new UnsupportedOperationException( + "Cannot call setReadPermissions after setPublishPermissions has been called."); + } + if (validatePermissions(permissions, SessionAuthorizationType.READ, session)) { + this.permissions = permissions; + authorizationType = SessionAuthorizationType.READ; + } + } + + public void setPublishPermissions(List permissions, Session session) { + if (SessionAuthorizationType.READ.equals(authorizationType)) { + throw new UnsupportedOperationException( + "Cannot call setPublishPermissions after setReadPermissions has been called."); + } + if (validatePermissions(permissions, SessionAuthorizationType.PUBLISH, session)) { + this.permissions = permissions; + authorizationType = SessionAuthorizationType.PUBLISH; + } + } + + private boolean validatePermissions(List permissions, + SessionAuthorizationType authType, Session currentSession) { + if (SessionAuthorizationType.PUBLISH.equals(authType)) { + if (Utility.isNullOrEmpty(permissions)) { + throw new IllegalArgumentException("Permissions for publish actions cannot be null or empty."); + } + } + if (currentSession != null && currentSession.isOpened()) { + if (!Utility.isSubset(permissions, currentSession.getPermissions())) { + Log.e(TAG, "Cannot set additional permissions when session is already open."); + return false; + } + } + return true; + } + + List getPermissions() { + return permissions; + } + + public void clearPermissions() { + permissions = null; + authorizationType = null; + } + + public void setLoginBehavior(SessionLoginBehavior loginBehavior) { + this.loginBehavior = loginBehavior; + } + + public SessionLoginBehavior getLoginBehavior() { + return loginBehavior; + } + + public void setSessionStatusCallback(Session.StatusCallback callback) { + this.sessionStatusCallback = callback; + } + + public Session.StatusCallback getSessionStatusCallback() { + return sessionStatusCallback; + } + } + + /** + * Specifies a callback interface that will be called when the button's notion of the current + * user changes (if the fetch_user_info attribute is true for this control). + */ + public interface UserInfoChangedCallback { + /** + * Called when the current user changes. + * @param user the current user, or null if there is no user + */ + void onUserInfoFetched(GraphUser user); + } + + /** + * Callback interface that will be called when a network or other error is encountered + * while logging in. + */ + public interface OnErrorListener { + /** + * Called when a network or other error is encountered. + * @param error a FacebookException representing the error that was encountered. + */ + void onError(FacebookException error); + } + + /** + * Create the LoginButton. + * + * @see View#View(Context) + */ + public LoginButton(Context context) { + super(context); + initializeActiveSessionWithCachedToken(context); + // since onFinishInflate won't be called, we need to finish initialization ourselves + finishInit(); + } + + /** + * Create the LoginButton by inflating from XML + * + * @see View#View(Context, AttributeSet) + */ + public LoginButton(Context context, AttributeSet attrs) { + super(context, attrs); + + if (attrs.getStyleAttribute() == 0) { + // apparently there's no method of setting a default style in xml, + // so in case the users do not explicitly specify a style, we need + // to use sensible defaults. + this.setGravity(Gravity.CENTER); + this.setTextColor(getResources().getColor(R.color.com_facebook_loginview_text_color)); + this.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.com_facebook_loginview_text_size)); + this.setTypeface(Typeface.DEFAULT_BOLD); + if (isInEditMode()) { + // cannot use a drawable in edit mode, so setting the background color instead + // of a background resource. + this.setBackgroundColor(getResources().getColor(R.color.com_facebook_blue)); + // hardcoding in edit mode as getResources().getString() doesn't seem to work in IntelliJ + loginText = "Log in with Facebook"; + } else { + this.setBackgroundResource(R.drawable.com_facebook_button_blue); + this.setCompoundDrawablesWithIntrinsicBounds(R.drawable.com_facebook_inverse_icon, 0, 0, 0); + this.setCompoundDrawablePadding( + getResources().getDimensionPixelSize(R.dimen.com_facebook_loginview_compound_drawable_padding)); + this.setPadding(getResources().getDimensionPixelSize(R.dimen.com_facebook_loginview_padding_left), + getResources().getDimensionPixelSize(R.dimen.com_facebook_loginview_padding_top), + getResources().getDimensionPixelSize(R.dimen.com_facebook_loginview_padding_right), + getResources().getDimensionPixelSize(R.dimen.com_facebook_loginview_padding_bottom)); + } + } + parseAttributes(attrs); + if (!isInEditMode()) { + initializeActiveSessionWithCachedToken(context); + } + } + + /** + * Create the LoginButton by inflating from XML and applying a style. + * + * @see View#View(Context, AttributeSet, int) + */ + public LoginButton(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + parseAttributes(attrs); + initializeActiveSessionWithCachedToken(context); + } + + /** + * Sets an OnErrorListener for this instance of LoginButton to call into when + * certain exceptions occur. + * + * @param onErrorListener The listener object to set + */ + public void setOnErrorListener(OnErrorListener onErrorListener) { + properties.setOnErrorListener(onErrorListener); + } + + /** + * Returns the current OnErrorListener for this instance of LoginButton. + * + * @return The OnErrorListener + */ + public OnErrorListener getOnErrorListener() { + return properties.getOnErrorListener(); + } + + /** + * Sets the default audience to use when the session is opened. + * This value is only useful when specifying write permissions for the native + * login dialog. + * + * @param defaultAudience the default audience value to use + */ + public void setDefaultAudience(SessionDefaultAudience defaultAudience) { + properties.setDefaultAudience(defaultAudience); + } + + /** + * Gets the default audience to use when the session is opened. + * This value is only useful when specifying write permissions for the native + * login dialog. + * + * @return the default audience value to use + */ + public SessionDefaultAudience getDefaultAudience() { + return properties.getDefaultAudience(); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * can only be read permissions. If any publish permissions are included, the login + * attempt by the user will fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the LoginButton is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setPublishPermissions has been called + */ + public void setReadPermissions(List permissions) { + properties.setReadPermissions(permissions, sessionTracker.getSession()); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * can only be read permissions. If any publish permissions are included, the login + * attempt by the user will fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the LoginButton is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setPublishPermissions has been called + */ + public void setReadPermissions(String... permissions) { + properties.setReadPermissions(Arrays.asList(permissions), sessionTracker.getSession()); + } + + + /** + * Set the permissions to use when the session is opened. The permissions here + * should only be publish permissions. If any read permissions are included, the login + * attempt by the user may fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the LoginButton is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setReadPermissions has been called + * @throws IllegalArgumentException if permissions is null or empty + */ + public void setPublishPermissions(List permissions) { + properties.setPublishPermissions(permissions, sessionTracker.getSession()); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * should only be publish permissions. If any read permissions are included, the login + * attempt by the user may fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the LoginButton is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setReadPermissions has been called + * @throws IllegalArgumentException if permissions is null or empty + */ + public void setPublishPermissions(String... permissions) { + properties.setPublishPermissions(Arrays.asList(permissions), sessionTracker.getSession()); + } + + + /** + * Clears the permissions currently associated with this LoginButton. + */ + public void clearPermissions() { + properties.clearPermissions(); + } + + /** + * Sets the login behavior for the session that will be opened. If null is specified, + * the default ({@link SessionLoginBehavior SessionLoginBehavior.SSO_WITH_FALLBACK} + * will be used. + * + * @param loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + */ + public void setLoginBehavior(SessionLoginBehavior loginBehavior) { + properties.setLoginBehavior(loginBehavior); + } + + /** + * Gets the login behavior for the session that will be opened. If null is returned, + * the default ({@link SessionLoginBehavior SessionLoginBehavior.SSO_WITH_FALLBACK} + * will be used. + * + * @return loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + */ + public SessionLoginBehavior getLoginBehavior() { + return properties.getLoginBehavior(); + } + + /** + * Set the application ID to be used to open the session. + * + * @param applicationId the application ID to use + */ + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + /** + * Gets the callback interface that will be called when the current user changes. + * @return the callback interface + */ + public UserInfoChangedCallback getUserInfoChangedCallback() { + return userInfoChangedCallback; + } + + /** + * Sets the callback interface that will be called when the current user changes. + * + * @param userInfoChangedCallback the callback interface + */ + public void setUserInfoChangedCallback(UserInfoChangedCallback userInfoChangedCallback) { + this.userInfoChangedCallback = userInfoChangedCallback; + } + + /** + * Sets the callback interface that will be called whenever the status of the Session + * associated with this LoginButton changes. Note that updates will only be sent to the + * callback while the LoginButton is actually attached to a window. + * + * @param callback the callback interface + */ + public void setSessionStatusCallback(Session.StatusCallback callback) { + properties.setSessionStatusCallback(callback); + } + + /** + * Sets the callback interface that will be called whenever the status of the Session + * associated with this LoginButton changes. + + * @return the callback interface + */ + public Session.StatusCallback getSessionStatusCallback() { + return properties.getSessionStatusCallback(); + } + + /** + * Sets the style (background) of the Tool Tip popup. Currently a blue style and a black + * style are supported. Blue is default + * @param nuxStyle The style of the tool tip popup. + */ + public void setToolTipStyle(ToolTipPopup.Style nuxStyle) { + this.nuxStyle = nuxStyle; + } + + /** + * Sets the mode of the Tool Tip popup. Currently supported modes are default (normal + * behavior), always_on (popup remains up until forcibly dismissed), and always_off (popup + * doesn't show) + * @param nuxMode The new mode for the tool tip + */ + public void setToolTipMode(ToolTipMode nuxMode) { + this.nuxMode = nuxMode; + } + + /** + * Return the current {@link ToolTipMode} for this LoginButton + * @return The {@link ToolTipMode} + */ + public ToolTipMode getToolTipMode() { + return nuxMode; + } + + /** + * Sets the amount of time (in milliseconds) that the tool tip will be shown to the user. The + * default is {@value ToolTipPopup#DEFAULT_POPUP_DISPLAY_TIME}. Any value that is less than or + * equal to zero will cause the tool tip to be displayed indefinitely. + * @param displayTime The amount of time (in milliseconds) that the tool tip will be displayed + * to the user + */ + public void setToolTipDisplayTime(long displayTime) { + this.nuxDisplayTime = displayTime; + } + + /** + * Gets the current amount of time (in ms) that the tool tip will be displayed to the user + * @return + */ + public long getToolTipDisplayTime() { + return nuxDisplayTime; + } + + /** + * Dismisses the Nux Tooltip if it is currently visible + */ + public void dismissToolTip() { + if (nuxPopup != null) { + nuxPopup.dismiss(); + nuxPopup = null; + } + } + + /** + * Provides an implementation for {@link Activity#onActivityResult + * onActivityResult} that updates the Session based on information returned + * during the authorization flow. The Activity containing this view + * should forward the resulting onActivityResult call here to + * update the Session state based on the contents of the resultCode and + * data. + * + * @param requestCode + * The requestCode parameter from the forwarded call. When this + * onActivityResult occurs as part of Facebook authorization + * flow, this value is the activityCode passed to open or + * authorize. + * @param resultCode + * An int containing the resultCode parameter from the forwarded + * call. + * @param data + * The Intent passed as the data parameter from the forwarded + * call. + * @return A boolean indicating whether the requestCode matched a pending + * authorization request for this Session. + * @see Session#onActivityResult(Activity, int, int, Intent) + */ + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + Session session = sessionTracker.getSession(); + if (session != null) { + return session.onActivityResult((Activity)getContext(), requestCode, + resultCode, data); + } else { + return false; + } + } + + /** + * Set the Session object to use instead of the active Session. Since a Session + * cannot be reused, if the user logs out from this Session, and tries to + * log in again, a new Active Session will be used instead. + *

+ * If the passed in session is currently opened, this method will also attempt to + * load some user information for display (if needed). + * + * @param newSession the Session object to use + * @throws FacebookException if errors occur during the loading of user information + */ + public void setSession(Session newSession) { + sessionTracker.setSession(newSession); + fetchUserInfo(); + setButtonText(); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + finishInit(); + } + + private void finishInit() { + super.setOnClickListener(new LoginClickListener()); + setButtonText(); + if (!isInEditMode()) { + sessionTracker = new SessionTracker(getContext(), new LoginButtonCallback(), null, false); + fetchUserInfo(); + } + } + + /** + * Sets the fragment that contains this control. This allows the LoginButton to be + * embedded inside a Fragment, and will allow the fragment to receive the + * {@link Fragment#onActivityResult(int, int, android.content.Intent) onActivityResult} + * call rather than the Activity. + * + * @param fragment the fragment that contains this control + */ + public void setFragment(Fragment fragment) { + parentFragment = fragment; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (sessionTracker != null && !sessionTracker.isTracking()) { + sessionTracker.startTracking(); + fetchUserInfo(); + setButtonText(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!nuxChecked && nuxMode != ToolTipMode.NEVER_DISPLAY && !isInEditMode()) { + nuxChecked = true; + checkNuxSettings(); + } + } + + private void showNuxPerSettings(FetchedAppSettings settings) { + if (settings != null && settings.getNuxEnabled() && getVisibility() == View.VISIBLE) { + String nuxString = settings.getNuxContent(); + displayNux(nuxString); + } + } + + private void displayNux(String nuxString) { + nuxPopup = new ToolTipPopup(nuxString, this); + nuxPopup.setStyle(nuxStyle); + nuxPopup.setNuxDisplayTime(nuxDisplayTime); + nuxPopup.show(); + } + + private void checkNuxSettings() { + if (nuxMode == ToolTipMode.DISPLAY_ALWAYS) { + String nuxString = getResources().getString(R.string.com_facebook_tooltip_default); + displayNux(nuxString); + } else { + // kick off an async request + final String appId = Utility.getMetadataApplicationId(getContext()); + AsyncTask task = new AsyncTask() { + @Override + protected FetchedAppSettings doInBackground(Void... params) { + FetchedAppSettings settings = Utility.queryAppSettings(appId, false); + return settings; + } + + @Override + protected void onPostExecute(FetchedAppSettings result) { + showNuxPerSettings(result); + } + }; + task.execute((Void[])null); + } + + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (sessionTracker != null) { + sessionTracker.stopTracking(); + } + dismissToolTip(); + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + // If the visibility is not VISIBLE, we want to dismiss the nux if it is there + if (visibility != VISIBLE) { + dismissToolTip(); + } + } + + // For testing purposes only + List getPermissions() { + return properties.getPermissions(); + } + + void setProperties(LoginButtonProperties properties) { + this.properties = properties; + } + + void setLoginLogoutEventName(String eventName) { + loginLogoutEventName = eventName; + } + + private void parseAttributes(AttributeSet attrs) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_login_view); + confirmLogout = a.getBoolean(R.styleable.com_facebook_login_view_confirm_logout, true); + fetchUserInfo = a.getBoolean(R.styleable.com_facebook_login_view_fetch_user_info, true); + loginText = a.getString(R.styleable.com_facebook_login_view_login_text); + logoutText = a.getString(R.styleable.com_facebook_login_view_logout_text); + a.recycle(); + } + + private void setButtonText() { + if (sessionTracker != null && sessionTracker.getOpenSession() != null) { + setText((logoutText != null) ? logoutText : + getResources().getString(R.string.com_facebook_loginview_log_out_button)); + } else { + setText((loginText != null) ? loginText : + getResources().getString(R.string.com_facebook_loginview_log_in_button)); + } + } + + private boolean initializeActiveSessionWithCachedToken(Context context) { + if (context == null) { + return false; + } + + Session session = Session.getActiveSession(); + if (session != null) { + return session.isOpened(); + } + + String applicationId = Utility.getMetadataApplicationId(context); + if (applicationId == null) { + return false; + } + + return Session.openActiveSessionFromCache(context) != null; + } + + private void fetchUserInfo() { + if (fetchUserInfo) { + final Session currentSession = sessionTracker.getOpenSession(); + if (currentSession != null) { + if (currentSession != userInfoSession) { + Request request = Request.newMeRequest(currentSession, new Request.GraphUserCallback() { + @Override + public void onCompleted(GraphUser me, Response response) { + if (currentSession == sessionTracker.getOpenSession()) { + user = me; + if (userInfoChangedCallback != null) { + userInfoChangedCallback.onUserInfoFetched(user); + } + } + if (response.getError() != null) { + handleError(response.getError().getException()); + } + } + }); + Request.executeBatchAsync(request); + userInfoSession = currentSession; + } + } else { + user = null; + if (userInfoChangedCallback != null) { + userInfoChangedCallback.onUserInfoFetched(user); + } + } + } + } + + /** + * Allow a developer to set the OnClickListener for the button. This will be called back after we do any handling + * internally for login + * @param clickListener + */ + @Override + public void setOnClickListener(OnClickListener clickListener) { + listenerCallback = clickListener; + } + + private class LoginClickListener implements OnClickListener { + + @Override + public void onClick(View v) { + Context context = getContext(); + final Session openSession = sessionTracker.getOpenSession(); + + if (openSession != null) { + // If the Session is currently open, it must mean we need to log out + if (confirmLogout) { + // Create a confirmation dialog + String logout = getResources().getString(R.string.com_facebook_loginview_log_out_action); + String cancel = getResources().getString(R.string.com_facebook_loginview_cancel_action); + String message; + if (user != null && user.getName() != null) { + message = String.format(getResources().getString(R.string.com_facebook_loginview_logged_in_as), user.getName()); + } else { + message = getResources().getString(R.string.com_facebook_loginview_logged_in_using_facebook); + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setMessage(message) + .setCancelable(true) + .setPositiveButton(logout, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + openSession.closeAndClearTokenInformation(); + } + }) + .setNegativeButton(cancel, null); + builder.create().show(); + } else { + openSession.closeAndClearTokenInformation(); + } + } else { + Session currentSession = sessionTracker.getSession(); + if (currentSession == null || currentSession.getState().isClosed()) { + sessionTracker.setSession(null); + Session session = new Session.Builder(context).setApplicationId(applicationId).build(); + Session.setActiveSession(session); + currentSession = session; + } + if (!currentSession.isOpened()) { + Session.OpenRequest openRequest = null; + if (parentFragment != null) { + openRequest = new Session.OpenRequest(parentFragment); + } else if (context instanceof Activity) { + openRequest = new Session.OpenRequest((Activity)context); + } + + if (openRequest != null) { + openRequest.setDefaultAudience(properties.defaultAudience); + openRequest.setPermissions(properties.permissions); + openRequest.setLoginBehavior(properties.loginBehavior); + + if (SessionAuthorizationType.PUBLISH.equals(properties.authorizationType)) { + currentSession.openForPublish(openRequest); + } else { + currentSession.openForRead(openRequest); + } + } + } + } + + AppEventsLogger logger = AppEventsLogger.newLogger(getContext()); + + Bundle parameters = new Bundle(); + parameters.putInt("logging_in", (openSession != null) ? 0 : 1); + + logger.logSdkEvent(loginLogoutEventName, null, parameters); + + if (listenerCallback != null) { + listenerCallback.onClick(v); + } + } + } + + private class LoginButtonCallback implements Session.StatusCallback { + @Override + public void call(Session session, SessionState state, + Exception exception) { + fetchUserInfo(); + setButtonText(); + + // if the client has a status callback registered, call it, otherwise + // call the default handleError method, but don't call both + if (properties.sessionStatusCallback != null) { + properties.sessionStatusCallback.call(session, state, exception); + } else if (exception != null) { + handleError(exception); + } + } + }; + + void handleError(Exception exception) { + if (properties.onErrorListener != null) { + if (exception instanceof FacebookException) { + properties.onErrorListener.onError((FacebookException)exception); + } else { + properties.onErrorListener.onError(new FacebookException(exception)); + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/PickerFragment.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/PickerFragment.java new file mode 100644 index 0000000000..7718a0c262 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/PickerFragment.java @@ -0,0 +1,1078 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.view.animation.AlphaAnimation; +import android.widget.*; +import com.facebook.*; +import com.facebook.android.R; +import com.facebook.model.GraphObject; +import com.facebook.internal.SessionTracker; + +import java.util.*; + +/** + * Provides functionality common to SDK UI elements that allow the user to pick one or more + * graph objects (e.g., places, friends) from a list of possibilities. The UI is exposed as a + * Fragment to allow to it to be included in an Activity along with other Fragments. The Fragments + * can be configured by passing parameters as part of their Intent bundle, or (for certain + * properties) by specifying attributes in their XML layout files. + *
+ * PickerFragments support callbacks that will be called in the event of an error, when the + * underlying data has been changed, or when the set of selected graph objects changes. + */ +public abstract class PickerFragment extends Fragment { + /** + * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the + * picker should show pictures (if available) for the graph objects. + */ + public static final String SHOW_PICTURES_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowPictures"; + /** + * The key for a String parameter in the fragment's Intent bundle to indicate which extra fields + * beyond the default fields should be retrieved for any graph objects in the results. + */ + public static final String EXTRA_FIELDS_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ExtraFields"; + /** + * The key for a boolean parameter in the fragment's Intent bundle to indicate whether the + * picker should display a title bar with a Done button. + */ + public static final String SHOW_TITLE_BAR_BUNDLE_KEY = "com.facebook.widget.PickerFragment.ShowTitleBar"; + /** + * The key for a String parameter in the fragment's Intent bundle to indicate the text to + * display in the title bar. + */ + public static final String TITLE_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.TitleText"; + /** + * The key for a String parameter in the fragment's Intent bundle to indicate the text to + * display in the Done btuton. + */ + public static final String DONE_BUTTON_TEXT_BUNDLE_KEY = "com.facebook.widget.PickerFragment.DoneButtonText"; + + private static final String SELECTION_BUNDLE_KEY = "com.facebook.android.PickerFragment.Selection"; + private static final String ACTIVITY_CIRCLE_SHOW_KEY = "com.facebook.android.PickerFragment.ActivityCircleShown"; + private static final int PROFILE_PICTURE_PREFETCH_BUFFER = 5; + + private final int layout; + private OnErrorListener onErrorListener; + private OnDataChangedListener onDataChangedListener; + private OnSelectionChangedListener onSelectionChangedListener; + private OnDoneButtonClickedListener onDoneButtonClickedListener; + private GraphObjectFilter filter; + private boolean showPictures = true; + private boolean showTitleBar = true; + private ListView listView; + HashSet extraFields = new HashSet(); + GraphObjectAdapter adapter; + private final Class graphObjectClass; + private LoadingStrategy loadingStrategy; + private SelectionStrategy selectionStrategy; + private ProgressBar activityCircle; + private SessionTracker sessionTracker; + private String titleText; + private String doneButtonText; + private TextView titleTextView; + private Button doneButton; + private Drawable titleBarBackground; + private Drawable doneButtonBackground; + private boolean appEventsLogged; + + PickerFragment(Class graphObjectClass, int layout, Bundle args) { + this.graphObjectClass = graphObjectClass; + this.layout = layout; + + setPickerFragmentSettingsFromBundle(args); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + adapter = createAdapter(); + adapter.setFilter(new GraphObjectAdapter.Filter() { + @Override + public boolean includeItem(T graphObject) { + return filterIncludesItem(graphObject); + } + }); + } + + @Override + public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { + super.onInflate(activity, attrs, savedInstanceState); + TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_picker_fragment); + + setShowPictures(a.getBoolean(R.styleable.com_facebook_picker_fragment_show_pictures, showPictures)); + String extraFieldsString = a.getString(R.styleable.com_facebook_picker_fragment_extra_fields); + if (extraFieldsString != null) { + String[] strings = extraFieldsString.split(","); + setExtraFields(Arrays.asList(strings)); + } + + showTitleBar = a.getBoolean(R.styleable.com_facebook_picker_fragment_show_title_bar, showTitleBar); + titleText = a.getString(R.styleable.com_facebook_picker_fragment_title_text); + doneButtonText = a.getString(R.styleable.com_facebook_picker_fragment_done_button_text); + titleBarBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_title_bar_background); + doneButtonBackground = a.getDrawable(R.styleable.com_facebook_picker_fragment_done_button_background); + + a.recycle(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + ViewGroup view = (ViewGroup) inflater.inflate(layout, container, false); + + listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + onListItemClick((ListView) parent, v, position); + } + }); + listView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + // We don't actually do anything differently on long-clicks, but setting the listener + // enables the selector transition that we have for visual consistency with the + // Facebook app's pickers. + return false; + } + }); + listView.setOnScrollListener(onScrollListener); + + activityCircle = (ProgressBar) view.findViewById(R.id.com_facebook_picker_activity_circle); + + setupViews(view); + + listView.setAdapter(adapter); + + return view; + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + sessionTracker = new SessionTracker(getActivity(), new Session.StatusCallback() { + @Override + public void call(Session session, SessionState state, Exception exception) { + if (!session.isOpened()) { + // When a session is closed, we want to clear out our data so it is not visible to subsequent users + clearResults(); + } + } + }); + + setSettingsFromBundle(savedInstanceState); + + loadingStrategy = createLoadingStrategy(); + loadingStrategy.attach(adapter); + + selectionStrategy = createSelectionStrategy(); + selectionStrategy.readSelectionFromBundle(savedInstanceState, SELECTION_BUNDLE_KEY); + + // Should we display a title bar? (We need to do this after we've retrieved our bundle settings.) + if (showTitleBar) { + inflateTitleBar((ViewGroup) getView()); + } + + if (activityCircle != null && savedInstanceState != null) { + boolean shown = savedInstanceState.getBoolean(ACTIVITY_CIRCLE_SHOW_KEY, false); + if (shown) { + displayActivityCircle(); + } else { + // Should be hidden already, but just to be sure. + hideActivityCircle(); + } + } + } + + @Override + public void onDetach() { + super.onDetach(); + + listView.setOnScrollListener(null); + listView.setAdapter(null); + + loadingStrategy.detach(); + sessionTracker.stopTracking(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + saveSettingsToBundle(outState); + selectionStrategy.saveSelectionToBundle(outState, SELECTION_BUNDLE_KEY); + if (activityCircle != null) { + outState.putBoolean(ACTIVITY_CIRCLE_SHOW_KEY, activityCircle.getVisibility() == View.VISIBLE); + } + } + + @Override + public void onStop() { + if (!appEventsLogged) { + logAppEvents(false); + } + super.onStop(); + } + + @Override + public void setArguments(Bundle args) { + super.setArguments(args); + setSettingsFromBundle(args); + } + + /** + * Gets the current OnDataChangedListener for this fragment, which will be called whenever + * the underlying data being displaying in the picker has changed. + * + * @return the OnDataChangedListener, or null if there is none + */ + public OnDataChangedListener getOnDataChangedListener() { + return onDataChangedListener; + } + + /** + * Sets the current OnDataChangedListener for this fragment, which will be called whenever + * the underlying data being displaying in the picker has changed. + * + * @param onDataChangedListener the OnDataChangedListener, or null if there is none + */ + public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) { + this.onDataChangedListener = onDataChangedListener; + } + + /** + * Gets the current OnSelectionChangedListener for this fragment, which will be called + * whenever the user selects or unselects a graph object in the list. + * + * @return the OnSelectionChangedListener, or null if there is none + */ + public OnSelectionChangedListener getOnSelectionChangedListener() { + return onSelectionChangedListener; + } + + /** + * Sets the current OnSelectionChangedListener for this fragment, which will be called + * whenever the user selects or unselects a graph object in the list. + * + * @param onSelectionChangedListener the OnSelectionChangedListener, or null if there is none + */ + public void setOnSelectionChangedListener( + OnSelectionChangedListener onSelectionChangedListener) { + this.onSelectionChangedListener = onSelectionChangedListener; + } + + /** + * Gets the current OnDoneButtonClickedListener for this fragment, which will be called + * when the user clicks the Done button. + * + * @return the OnDoneButtonClickedListener, or null if there is none + */ + public OnDoneButtonClickedListener getOnDoneButtonClickedListener() { + return onDoneButtonClickedListener; + } + + /** + * Sets the current OnDoneButtonClickedListener for this fragment, which will be called + * when the user clicks the Done button. This will only be possible if the title bar is + * being shown in this fragment. + * + * @param onDoneButtonClickedListener the OnDoneButtonClickedListener, or null if there is none + */ + public void setOnDoneButtonClickedListener(OnDoneButtonClickedListener onDoneButtonClickedListener) { + this.onDoneButtonClickedListener = onDoneButtonClickedListener; + } + + /** + * Gets the current OnErrorListener for this fragment, which will be called in the event + * of network or other errors encountered while populating the graph objects in the list. + * + * @return the OnErrorListener, or null if there is none + */ + public OnErrorListener getOnErrorListener() { + return onErrorListener; + } + + /** + * Sets the current OnErrorListener for this fragment, which will be called in the event + * of network or other errors encountered while populating the graph objects in the list. + * + * @param onErrorListener the OnErrorListener, or null if there is none + */ + public void setOnErrorListener(OnErrorListener onErrorListener) { + this.onErrorListener = onErrorListener; + } + + /** + * Gets the current filter for this fragment, which will be called for each graph object + * returned from the service to determine if it should be displayed in the list. + * If no filter is specified, all retrieved graph objects will be displayed. + * + * @return the GraphObjectFilter, or null if there is none + */ + public GraphObjectFilter getFilter() { + return filter; + } + + /** + * Sets the current filter for this fragment, which will be called for each graph object + * returned from the service to determine if it should be displayed in the list. + * If no filter is specified, all retrieved graph objects will be displayed. + * + * @param filter the GraphObjectFilter, or null if there is none + */ + public void setFilter(GraphObjectFilter filter) { + this.filter = filter; + } + + /** + * Gets the Session to use for any Facebook requests this fragment will make. + * + * @return the Session that will be used for any Facebook requests, or null if there is none + */ + public Session getSession() { + return sessionTracker.getSession(); + } + + /** + * Sets the Session to use for any Facebook requests this fragment will make. If the + * parameter is null, the fragment will use the current active session, if any. + * + * @param session the Session to use for Facebook requests, or null to use the active session + */ + public void setSession(Session session) { + sessionTracker.setSession(session); + } + + /** + * Gets whether to display pictures, if available, for displayed graph objects. + * + * @return true if pictures should be displayed, false if not + */ + public boolean getShowPictures() { + return showPictures; + } + + /** + * Sets whether to display pictures, if available, for displayed graph objects. + * + * @param showPictures true if pictures should be displayed, false if not + */ + public void setShowPictures(boolean showPictures) { + this.showPictures = showPictures; + } + + /** + * Gets the extra fields to request for the retrieved graph objects. + * + * @return the extra fields to request + */ + public Set getExtraFields() { + return new HashSet(extraFields); + } + + /** + * Sets the extra fields to request for the retrieved graph objects. + * + * @param fields the extra fields to request + */ + public void setExtraFields(Collection fields) { + extraFields = new HashSet(); + if (fields != null) { + extraFields.addAll(fields); + } + } + + /** + * Sets whether to show a title bar with a Done button. This must be + * called prior to the Fragment going through its creation lifecycle to have an effect. + * + * @param showTitleBar true if a title bar should be displayed, false if not + */ + public void setShowTitleBar(boolean showTitleBar) { + this.showTitleBar = showTitleBar; + } + + /** + * Gets whether to show a title bar with a Done button. The default is true. + * + * @return true if a title bar will be shown, false if not. + */ + public boolean getShowTitleBar() { + return showTitleBar; + } + + /** + * Sets the text to show in the title bar, if a title bar is to be shown. This must be + * called prior to the Fragment going through its creation lifecycle to have an effect, or + * the default will be used. + * + * @param titleText the text to show in the title bar + */ + public void setTitleText(String titleText) { + this.titleText = titleText; + } + + /** + * Gets the text to show in the title bar, if a title bar is to be shown. + * + * @return the text to show in the title bar + */ + public String getTitleText() { + if (titleText == null) { + titleText = getDefaultTitleText(); + } + return titleText; + } + + /** + * Sets the text to show in the Done button, if a title bar is to be shown. This must be + * called prior to the Fragment going through its creation lifecycle to have an effect, or + * the default will be used. + * + * @param doneButtonText the text to show in the Done button + */ + public void setDoneButtonText(String doneButtonText) { + this.doneButtonText = doneButtonText; + } + + /** + * Gets the text to show in the Done button, if a title bar is to be shown. + * + * @return the text to show in the Done button + */ + public String getDoneButtonText() { + if (doneButtonText == null) { + doneButtonText = getDefaultDoneButtonText(); + } + return doneButtonText; + } + + /** + * Causes the picker to load data from the service and display it to the user. + * + * @param forceReload if true, data will be loaded even if there is already data being displayed (or loading); + * if false, data will not be re-loaded if it is already displayed (or loading) + */ + public void loadData(boolean forceReload) { + if (!forceReload && loadingStrategy.isDataPresentOrLoading()) { + return; + } + loadDataSkippingRoundTripIfCached(); + } + + /** + * Updates the properties of the PickerFragment based on the contents of the supplied Bundle; + * calling Activities may use this to pass additional configuration information to the + * PickerFragment beyond what is specified in its XML layout. + * + * @param inState a Bundle containing keys corresponding to properties of the PickerFragment + */ + public void setSettingsFromBundle(Bundle inState) { + setPickerFragmentSettingsFromBundle(inState); + } + + void setupViews(ViewGroup view) { + } + + boolean filterIncludesItem(T graphObject) { + if (filter != null) { + return filter.includeItem(graphObject); + } + return true; + } + + List getSelectedGraphObjects() { + return adapter.getGraphObjectsById(selectionStrategy.getSelectedIds()); + } + + void setSelectedGraphObjects(List objectIds) { + for(String objectId : objectIds) { + if(!this.selectionStrategy.isSelected(objectId)) { + this.selectionStrategy.toggleSelection(objectId); + } + } + } + + void saveSettingsToBundle(Bundle outState) { + outState.putBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures); + if (!extraFields.isEmpty()) { + outState.putString(EXTRA_FIELDS_BUNDLE_KEY, TextUtils.join(",", extraFields)); + } + outState.putBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar); + outState.putString(TITLE_TEXT_BUNDLE_KEY, titleText); + outState.putString(DONE_BUTTON_TEXT_BUNDLE_KEY, doneButtonText); + } + + abstract Request getRequestForLoadData(Session session); + + abstract PickerFragmentAdapter createAdapter(); + + abstract LoadingStrategy createLoadingStrategy(); + + abstract SelectionStrategy createSelectionStrategy(); + + void onLoadingData() { + } + + String getDefaultTitleText() { + return null; + } + + String getDefaultDoneButtonText() { + return getString(R.string.com_facebook_picker_done_button_text); + } + + void displayActivityCircle() { + if (activityCircle != null) { + layoutActivityCircle(); + activityCircle.setVisibility(View.VISIBLE); + } + } + + void layoutActivityCircle() { + // If we've got no data, make the activity circle full-opacity. Otherwise we'll dim it to avoid + // cluttering the UI. + float alpha = (!adapter.isEmpty()) ? .25f : 1.0f; + setAlpha(activityCircle, alpha); + } + + void hideActivityCircle() { + if (activityCircle != null) { + // We use an animation to dim the activity circle; need to clear this or it will remain visible. + activityCircle.clearAnimation(); + activityCircle.setVisibility(View.INVISIBLE); + } + } + + void setSelectionStrategy(SelectionStrategy selectionStrategy) { + if (selectionStrategy != this.selectionStrategy) { + this.selectionStrategy = selectionStrategy; + if (adapter != null) { + // Adapter should cause a re-render. + adapter.notifyDataSetChanged(); + } + } + } + + void logAppEvents(boolean doneButtonClicked) { + } + + private static void setAlpha(View view, float alpha) { + // Set the alpha appropriately (setAlpha is API >= 11, this technique works on all API levels). + AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha); + alphaAnimation.setDuration(0); + alphaAnimation.setFillAfter(true); + view.startAnimation(alphaAnimation); + } + + + private void setPickerFragmentSettingsFromBundle(Bundle inState) { + // We do this in a separate non-overridable method so it is safe to call from the constructor. + if (inState != null) { + showPictures = inState.getBoolean(SHOW_PICTURES_BUNDLE_KEY, showPictures); + String extraFieldsString = inState.getString(EXTRA_FIELDS_BUNDLE_KEY); + if (extraFieldsString != null) { + String[] strings = extraFieldsString.split(","); + setExtraFields(Arrays.asList(strings)); + } + showTitleBar = inState.getBoolean(SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar); + String titleTextString = inState.getString(TITLE_TEXT_BUNDLE_KEY); + if (titleTextString != null) { + titleText = titleTextString; + if (titleTextView != null) { + titleTextView.setText(titleText); + } + } + String doneButtonTextString = inState.getString(DONE_BUTTON_TEXT_BUNDLE_KEY); + if (doneButtonTextString != null) { + doneButtonText = doneButtonTextString; + if (doneButton != null) { + doneButton.setText(doneButtonText); + } + } + } + } + + private void inflateTitleBar(ViewGroup view) { + ViewStub stub = (ViewStub) view.findViewById(R.id.com_facebook_picker_title_bar_stub); + if (stub != null) { + View titleBar = stub.inflate(); + + final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.MATCH_PARENT); + layoutParams.addRule(RelativeLayout.BELOW, R.id.com_facebook_picker_title_bar); + listView.setLayoutParams(layoutParams); + + if (titleBarBackground != null) { + titleBar.setBackgroundDrawable(titleBarBackground); + } + + doneButton = (Button) view.findViewById(R.id.com_facebook_picker_done_button); + if (doneButton != null) { + doneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + logAppEvents(true); + appEventsLogged = true; + + if (onDoneButtonClickedListener != null) { + onDoneButtonClickedListener.onDoneButtonClicked(PickerFragment.this); + } + } + }); + + if (getDoneButtonText() != null) { + doneButton.setText(getDoneButtonText()); + } + + if (doneButtonBackground != null) { + doneButton.setBackgroundDrawable(doneButtonBackground); + } + } + + titleTextView = (TextView) view.findViewById(R.id.com_facebook_picker_title); + if (titleTextView != null) { + if (getTitleText() != null) { + titleTextView.setText(getTitleText()); + } + } + } + } + + private void onListItemClick(ListView listView, View v, int position) { + @SuppressWarnings("unchecked") + T graphObject = (T) listView.getItemAtPosition(position); + String id = adapter.getIdOfGraphObject(graphObject); + selectionStrategy.toggleSelection(id); + adapter.notifyDataSetChanged(); + + if (onSelectionChangedListener != null) { + onSelectionChangedListener.onSelectionChanged(PickerFragment.this); + } + } + + private void loadDataSkippingRoundTripIfCached() { + clearResults(); + + Request request = getRequestForLoadData(getSession()); + if (request != null) { + onLoadingData(); + loadingStrategy.startLoading(request); + } + } + + private void clearResults() { + if (adapter != null) { + boolean wasSelection = !selectionStrategy.isEmpty(); + boolean wasData = !adapter.isEmpty(); + + loadingStrategy.clearResults(); + selectionStrategy.clear(); + adapter.notifyDataSetChanged(); + + // Tell anyone who cares the data and selection has changed, if they have. + if (wasData && onDataChangedListener != null) { + onDataChangedListener.onDataChanged(PickerFragment.this); + } + if (wasSelection && onSelectionChangedListener != null) { + onSelectionChangedListener.onSelectionChanged(PickerFragment.this); + } + } + } + + void updateAdapter(SimpleGraphObjectCursor data) { + if (adapter != null) { + // As we fetch additional results and add them to the table, we do not + // want the items displayed jumping around seemingly at random, frustrating the user's + // attempts at scrolling, etc. Since results may be added anywhere in + // the table, we choose to try to keep the first visible row in a fixed + // position (from the user's perspective). We try to keep it positioned at + // the same offset from the top of the screen so adding new items seems + // smoother, as opposed to having it "snap" to a multiple of row height + + // We use the second row, to give context above and below it and avoid + // cases where the first row is only barely visible, thus providing little context. + // The exception is where the very first row is visible, in which case we use that. + View view = listView.getChildAt(1); + int anchorPosition = listView.getFirstVisiblePosition(); + if (anchorPosition > 0) { + anchorPosition++; + } + GraphObjectAdapter.SectionAndItem anchorItem = adapter.getSectionAndItem(anchorPosition); + final int top = (view != null && + anchorItem.getType() != GraphObjectAdapter.SectionAndItem.Type.ACTIVITY_CIRCLE) ? + view.getTop() : 0; + + // Now actually add the results. + boolean dataChanged = adapter.changeCursor(data); + + if (view != null && anchorItem != null) { + // Put the item back in the same spot it was. + final int newPositionOfItem = adapter.getPosition(anchorItem.sectionKey, anchorItem.graphObject); + if (newPositionOfItem != -1) { + listView.setSelectionFromTop(newPositionOfItem, top); + } + } + + if (dataChanged && onDataChangedListener != null) { + onDataChangedListener.onDataChanged(PickerFragment.this); + } + } + } + + private void reprioritizeDownloads() { + int lastVisibleItem = listView.getLastVisiblePosition(); + if (lastVisibleItem >= 0) { + int firstVisibleItem = listView.getFirstVisiblePosition(); + adapter.prioritizeViewRange(firstVisibleItem, lastVisibleItem, PROFILE_PICTURE_PREFETCH_BUFFER); + } + } + + private ListView.OnScrollListener onScrollListener = new ListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + reprioritizeDownloads(); + } + }; + + /** + * Callback interface that will be called when a network or other error is encountered + * while retrieving graph objects. + */ + public interface OnErrorListener { + /** + * Called when a network or other error is encountered. + * + * @param error a FacebookException representing the error that was encountered. + */ + void onError(PickerFragment fragment, FacebookException error); + } + + /** + * Callback interface that will be called when the underlying data being displayed in the + * picker has been updated. + */ + public interface OnDataChangedListener { + /** + * Called when the set of data being displayed in the picker has changed. + */ + void onDataChanged(PickerFragment fragment); + } + + /** + * Callback interface that will be called when the user selects or unselects graph objects + * in the picker. + */ + public interface OnSelectionChangedListener { + /** + * Called when the user selects or unselects graph objects in the picker. + */ + void onSelectionChanged(PickerFragment fragment); + } + + /** + * Callback interface that will be called when the user clicks the Done button on the + * title bar. + */ + public interface OnDoneButtonClickedListener { + /** + * Called when the user clicks the Done button. + */ + void onDoneButtonClicked(PickerFragment fragment); + } + + /** + * Callback interface that will be called to determine if a graph object should be displayed. + * + * @param + */ + public interface GraphObjectFilter { + /** + * Called to determine if a graph object should be displayed. + * + * @param graphObject the graph object + * @return true to display the graph object, false to hide it + */ + boolean includeItem(T graphObject); + } + + abstract class LoadingStrategy { + protected final static int CACHED_RESULT_REFRESH_DELAY = 2 * 1000; + + protected GraphObjectPagingLoader loader; + protected GraphObjectAdapter adapter; + + public void attach(GraphObjectAdapter adapter) { + loader = (GraphObjectPagingLoader) getLoaderManager().initLoader(0, null, + new LoaderManager.LoaderCallbacks>() { + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return LoadingStrategy.this.onCreateLoader(); + } + + @Override + public void onLoadFinished(Loader> loader, + SimpleGraphObjectCursor data) { + if (loader != LoadingStrategy.this.loader) { + throw new FacebookException("Received callback for unknown loader."); + } + LoadingStrategy.this.onLoadFinished((GraphObjectPagingLoader) loader, data); + } + + @Override + public void onLoaderReset(Loader> loader) { + if (loader != LoadingStrategy.this.loader) { + throw new FacebookException("Received callback for unknown loader."); + } + LoadingStrategy.this.onLoadReset((GraphObjectPagingLoader) loader); + } + }); + + loader.setOnErrorListener(new GraphObjectPagingLoader.OnErrorListener() { + @Override + public void onError(FacebookException error, GraphObjectPagingLoader loader) { + hideActivityCircle(); + if (onErrorListener != null) { + onErrorListener.onError(PickerFragment.this, error); + } + } + }); + + this.adapter = adapter; + // Tell the adapter about any data we might already have. + this.adapter.changeCursor(loader.getCursor()); + this.adapter.setOnErrorListener(new GraphObjectAdapter.OnErrorListener() { + @Override + public void onError(GraphObjectAdapter adapter, FacebookException error) { + if (onErrorListener != null) { + onErrorListener.onError(PickerFragment.this, error); + } + } + }); + } + + public void detach() { + adapter.setDataNeededListener(null); + adapter.setOnErrorListener(null); + loader.setOnErrorListener(null); + + loader = null; + adapter = null; + } + + public void clearResults() { + if (loader != null) { + loader.clearResults(); + } + } + + public void startLoading(Request request) { + if (loader != null) { + loader.startLoading(request, true); + onStartLoading(loader, request); + } + } + + public boolean isDataPresentOrLoading() { + return !adapter.isEmpty() || loader.isLoading(); + } + + protected GraphObjectPagingLoader onCreateLoader() { + return new GraphObjectPagingLoader(getActivity(), graphObjectClass); + } + + protected void onStartLoading(GraphObjectPagingLoader loader, Request request) { + displayActivityCircle(); + } + + protected void onLoadReset(GraphObjectPagingLoader loader) { + adapter.changeCursor(null); + } + + protected void onLoadFinished(GraphObjectPagingLoader loader, SimpleGraphObjectCursor data) { + updateAdapter(data); + } + } + + abstract class SelectionStrategy { + abstract boolean isSelected(String id); + + abstract void toggleSelection(String id); + + abstract Collection getSelectedIds(); + + abstract void clear(); + + abstract boolean isEmpty(); + + abstract boolean shouldShowCheckBoxIfUnselected(); + + abstract void saveSelectionToBundle(Bundle outBundle, String key); + + abstract void readSelectionFromBundle(Bundle inBundle, String key); + } + + class SingleSelectionStrategy extends SelectionStrategy { + private String selectedId; + + public Collection getSelectedIds() { + return Arrays.asList(new String[]{selectedId}); + } + + @Override + boolean isSelected(String id) { + return selectedId != null && id != null && selectedId.equals(id); + } + + @Override + void toggleSelection(String id) { + if (selectedId != null && selectedId.equals(id)) { + selectedId = null; + } else { + selectedId = id; + } + } + + @Override + void saveSelectionToBundle(Bundle outBundle, String key) { + if (!TextUtils.isEmpty(selectedId)) { + outBundle.putString(key, selectedId); + } + } + + @Override + void readSelectionFromBundle(Bundle inBundle, String key) { + if (inBundle != null) { + selectedId = inBundle.getString(key); + } + } + + @Override + public void clear() { + selectedId = null; + } + + @Override + boolean isEmpty() { + return selectedId == null; + } + + @Override + boolean shouldShowCheckBoxIfUnselected() { + return false; + } + } + + class MultiSelectionStrategy extends SelectionStrategy { + private Set selectedIds = new HashSet(); + + public Collection getSelectedIds() { + return selectedIds; + } + + @Override + boolean isSelected(String id) { + return id != null && selectedIds.contains(id); + } + + @Override + void toggleSelection(String id) { + if (id != null) { + if (selectedIds.contains(id)) { + selectedIds.remove(id); + } else { + selectedIds.add(id); + } + } + } + + @Override + void saveSelectionToBundle(Bundle outBundle, String key) { + if (!selectedIds.isEmpty()) { + String ids = TextUtils.join(",", selectedIds); + outBundle.putString(key, ids); + } + } + + @Override + void readSelectionFromBundle(Bundle inBundle, String key) { + if (inBundle != null) { + String ids = inBundle.getString(key); + if (ids != null) { + String[] splitIds = TextUtils.split(ids, ","); + selectedIds.clear(); + Collections.addAll(selectedIds, splitIds); + } + } + } + + @Override + public void clear() { + selectedIds.clear(); + } + + @Override + boolean isEmpty() { + return selectedIds.isEmpty(); + } + + @Override + boolean shouldShowCheckBoxIfUnselected() { + return true; + } + } + + abstract class PickerFragmentAdapter extends GraphObjectAdapter { + public PickerFragmentAdapter(Context context) { + super(context); + } + + @Override + boolean isGraphObjectSelected(String graphObjectId) { + return selectionStrategy.isSelected(graphObjectId); + } + + @Override + void updateCheckboxState(CheckBox checkBox, boolean graphObjectSelected) { + checkBox.setChecked(graphObjectSelected); + int visible = (graphObjectSelected || selectionStrategy + .shouldShowCheckBoxIfUnselected()) ? View.VISIBLE : View.GONE; + checkBox.setVisibility(visible); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/PlacePickerFragment.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/PlacePickerFragment.java new file mode 100644 index 0000000000..58b277583c --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/PlacePickerFragment.java @@ -0,0 +1,528 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.location.Location; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ListView; +import com.facebook.*; +import com.facebook.android.R; +import com.facebook.internal.AnalyticsEvents; +import com.facebook.model.GraphPlace; +import com.facebook.internal.Logger; +import com.facebook.internal.Utility; + +import java.util.*; + +public class PlacePickerFragment extends PickerFragment { + /** + * The key for an int parameter in the fragment's Intent bundle to indicate the radius in meters around + * the center point to search. The default is 1000 meters. + */ + public static final String RADIUS_IN_METERS_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.RadiusInMeters"; + /** + * The key for an int parameter in the fragment's Intent bundle to indicate what how many results to + * return at a time. The default is 100 results. + */ + public static final String RESULTS_LIMIT_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.ResultsLimit"; + /** + * The key for a String parameter in the fragment's Intent bundle to indicate what search text should + * be sent to the service. The default is to have no search text. + */ + public static final String SEARCH_TEXT_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.SearchText"; + /** + * The key for a Location parameter in the fragment's Intent bundle to indicate what geographical + * location should be the center of the search. + */ + public static final String LOCATION_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.Location"; + /** + * The key for a boolean parameter in the fragment's Intent bundle to indicate that the fragment + * should display a search box and automatically update the search text as it changes. + */ + public static final String SHOW_SEARCH_BOX_BUNDLE_KEY = "com.facebook.widget.PlacePickerFragment.ShowSearchBox"; + + /** + * The default radius around the center point to search. + */ + public static final int DEFAULT_RADIUS_IN_METERS = 1000; + /** + * The default number of results to retrieve. + */ + public static final int DEFAULT_RESULTS_LIMIT = 100; + + private static final int searchTextTimerDelayInMilliseconds = 2 * 1000; + + private static final String ID = "id"; + private static final String NAME = "name"; + private static final String LOCATION = "location"; + private static final String CATEGORY = "category"; + private static final String WERE_HERE_COUNT = "were_here_count"; + private static final String TAG = "PlacePickerFragment"; + + private Location location; + private int radiusInMeters = DEFAULT_RADIUS_IN_METERS; + private int resultsLimit = DEFAULT_RESULTS_LIMIT; + private String searchText; + private Timer searchTextTimer; + private boolean hasSearchTextChangedSinceLastQuery; + private boolean showSearchBox = true; + private EditText searchBox; + + /** + * Default constructor. Creates a Fragment with all default properties. + */ + public PlacePickerFragment() { + this(null); + } + + /** + * Constructor. + * + * @param args a Bundle that optionally contains one or more values containing additional + * configuration information for the Fragment. + */ + public PlacePickerFragment(Bundle args) { + super(GraphPlace.class, R.layout.com_facebook_placepickerfragment, args); + setPlacePickerSettingsFromBundle(args); + } + + /** + * Gets the location to search around. Either the location or the search text (or both) must be specified. + * + * @return the Location to search around + */ + public Location getLocation() { + return location; + } + + /** + * Sets the location to search around. Either the location or the search text (or both) must be specified. + * + * @param location the Location to search around + */ + public void setLocation(Location location) { + this.location = location; + } + + /** + * Gets the radius in meters around the location to search. + * + * @return the radius in meters + */ + public int getRadiusInMeters() { + return radiusInMeters; + } + + /** + * Sets the radius in meters around the location to search. + * + * @param radiusInMeters the radius in meters + */ + public void setRadiusInMeters(int radiusInMeters) { + this.radiusInMeters = radiusInMeters; + } + + /** + * Gets the number of results to retrieve. + * + * @return the number of results to retrieve + */ + public int getResultsLimit() { + return resultsLimit; + } + + /** + * Sets the number of results to retrieve. + * + * @param resultsLimit the number of results to retrieve + */ + public void setResultsLimit(int resultsLimit) { + this.resultsLimit = resultsLimit; + } + + /** + * Gets the search text (e.g., category, name) to search for. Either the location or the search + * text (or both) must be specified. + * + * @return the search text + */ + public String getSearchText() { + return searchText; + } + + /** + * Sets the search text (e.g., category, name) to search for. Either the location or the search + * text (or both) must be specified. If a search box is displayed, this will update its contents + * to the specified text. + * + * @param searchText the search text + */ + public void setSearchText(String searchText) { + if (TextUtils.isEmpty(searchText)) { + searchText = null; + } + this.searchText = searchText; + if (this.searchBox != null) { + this.searchBox.setText(searchText); + } + } + + /** + * Sets the search text and reloads the data in the control. This is used to provide search-box + * functionality where the user may be typing or editing text rapidly. It uses a timer to avoid repeated + * requerying, preferring to wait until the user pauses typing to refresh the data. Note that this + * method will NOT update the text in the search box, if any, as it is intended to be called as a result + * of changes to the search box (and is public to enable applications to provide their own search box + * UI instead of the default one). + * + * @param searchText the search text + * @param forceReloadEventIfSameText if true, will reload even if the search text has not changed; if false, + * identical search text will not force a reload + */ + public void onSearchBoxTextChanged(String searchText, boolean forceReloadEventIfSameText) { + if (!forceReloadEventIfSameText && Utility.stringsEqualOrEmpty(this.searchText, searchText)) { + return; + } + + if (TextUtils.isEmpty(searchText)) { + searchText = null; + } + this.searchText = searchText; + + // If search text is being set in response to user input, it is wasteful to send a new request + // with every keystroke. Send a request the first time the search text is set, then set up a 2-second timer + // and send whatever changes the user has made since then. (If nothing has changed + // in 2 seconds, we reset so the next change will cause an immediate re-query.) + hasSearchTextChangedSinceLastQuery = true; + if (searchTextTimer == null) { + searchTextTimer = createSearchTextTimer(); + } + } + + /** + * Gets the currently-selected place. + * + * @return the currently-selected place, or null if there is none + */ + public GraphPlace getSelection() { + Collection selection = getSelectedGraphObjects(); + return (selection != null && !selection.isEmpty()) ? selection.iterator().next() : null; + } + + public void setSettingsFromBundle(Bundle inState) { + super.setSettingsFromBundle(inState); + setPlacePickerSettingsFromBundle(inState); + } + + @Override + public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { + super.onInflate(activity, attrs, savedInstanceState); + TypedArray a = activity.obtainStyledAttributes(attrs, R.styleable.com_facebook_place_picker_fragment); + + setRadiusInMeters(a.getInt(R.styleable.com_facebook_place_picker_fragment_radius_in_meters, radiusInMeters)); + setResultsLimit(a.getInt(R.styleable.com_facebook_place_picker_fragment_results_limit, resultsLimit)); + if (a.hasValue(R.styleable.com_facebook_place_picker_fragment_results_limit)) { + setSearchText(a.getString(R.styleable.com_facebook_place_picker_fragment_search_text)); + } + showSearchBox = a.getBoolean(R.styleable.com_facebook_place_picker_fragment_show_search_box, showSearchBox); + + a.recycle(); + } + + @Override + void setupViews(ViewGroup view) { + if (showSearchBox) { + ListView listView = (ListView) view.findViewById(R.id.com_facebook_picker_list_view); + + View searchHeaderView = getActivity().getLayoutInflater().inflate( + R.layout.com_facebook_picker_search_box, listView, false); + + listView.addHeaderView(searchHeaderView, null, false); + + searchBox = (EditText) view.findViewById(R.id.com_facebook_picker_search_text); + + searchBox.addTextChangedListener(new SearchTextWatcher()); + if (!TextUtils.isEmpty(searchText)) { + searchBox.setText(searchText); + } + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (searchBox != null) { + InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(searchBox, InputMethodManager.SHOW_IMPLICIT); + } + } + + @Override + public void onDetach() { + super.onDetach(); + + if (searchBox != null) { + InputMethodManager imm = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(searchBox.getWindowToken(), 0); + } + } + + void saveSettingsToBundle(Bundle outState) { + super.saveSettingsToBundle(outState); + + outState.putInt(RADIUS_IN_METERS_BUNDLE_KEY, radiusInMeters); + outState.putInt(RESULTS_LIMIT_BUNDLE_KEY, resultsLimit); + outState.putString(SEARCH_TEXT_BUNDLE_KEY, searchText); + outState.putParcelable(LOCATION_BUNDLE_KEY, location); + outState.putBoolean(SHOW_SEARCH_BOX_BUNDLE_KEY, showSearchBox); + } + + @Override + void onLoadingData() { + hasSearchTextChangedSinceLastQuery = false; + } + + @Override + Request getRequestForLoadData(Session session) { + return createRequest(location, radiusInMeters, resultsLimit, searchText, extraFields, session); + } + + @Override + String getDefaultTitleText() { + return getString(R.string.com_facebook_nearby); + } + + @Override + void logAppEvents(boolean doneButtonClicked) { + AppEventsLogger logger = AppEventsLogger.newLogger(this.getActivity(), getSession()); + Bundle parameters = new Bundle(); + + // If Done was clicked, we know this completed successfully. If not, we don't know (caller might have + // dismissed us in response to selection changing, or user might have hit back button). Either way + // we'll log the number of selections. + String outcome = doneButtonClicked ? AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_COMPLETED : + AnalyticsEvents.PARAMETER_DIALOG_OUTCOME_VALUE_UNKNOWN; + parameters.putString(AnalyticsEvents.PARAMETER_DIALOG_OUTCOME, outcome); + parameters.putInt("num_places_picked", (getSelection() != null) ? 1 : 0); + + logger.logSdkEvent(AnalyticsEvents.EVENT_PLACE_PICKER_USAGE, null, parameters); + } + + @Override + PickerFragmentAdapter createAdapter() { + PickerFragmentAdapter adapter = new PickerFragmentAdapter( + this.getActivity()) { + @Override + protected CharSequence getSubTitleOfGraphObject(GraphPlace graphObject) { + String category = graphObject.getCategory(); + Integer wereHereCount = (Integer) graphObject.getProperty(WERE_HERE_COUNT); + + String result = null; + if (category != null && wereHereCount != null) { + result = getString(R.string.com_facebook_placepicker_subtitle_format, category, wereHereCount); + } else if (category == null && wereHereCount != null) { + result = getString(R.string.com_facebook_placepicker_subtitle_were_here_only_format, wereHereCount); + } else if (category != null && wereHereCount == null) { + result = getString(R.string.com_facebook_placepicker_subtitle_catetory_only_format, category); + } + return result; + } + + @Override + protected int getGraphObjectRowLayoutId(GraphPlace graphObject) { + return R.layout.com_facebook_placepickerfragment_list_row; + } + + @Override + protected int getDefaultPicture() { + return R.drawable.com_facebook_place_default_icon; + } + + }; + adapter.setShowCheckbox(false); + adapter.setShowPicture(getShowPictures()); + return adapter; + } + + @Override + LoadingStrategy createLoadingStrategy() { + return new AsNeededLoadingStrategy(); + } + + @Override + SelectionStrategy createSelectionStrategy() { + return new SingleSelectionStrategy(); + } + + private Request createRequest(Location location, int radiusInMeters, int resultsLimit, String searchText, + Set extraFields, + Session session) { + Request request = Request.newPlacesSearchRequest(session, location, radiusInMeters, resultsLimit, searchText, + null); + + Set fields = new HashSet(extraFields); + String[] requiredFields = new String[]{ + ID, + NAME, + LOCATION, + CATEGORY, + WERE_HERE_COUNT + }; + fields.addAll(Arrays.asList(requiredFields)); + + String pictureField = adapter.getPictureFieldSpecifier(); + if (pictureField != null) { + fields.add(pictureField); + } + + Bundle parameters = request.getParameters(); + parameters.putString("fields", TextUtils.join(",", fields)); + request.setParameters(parameters); + + return request; + } + + private void setPlacePickerSettingsFromBundle(Bundle inState) { + // We do this in a separate non-overridable method so it is safe to call from the constructor. + if (inState != null) { + setRadiusInMeters(inState.getInt(RADIUS_IN_METERS_BUNDLE_KEY, radiusInMeters)); + setResultsLimit(inState.getInt(RESULTS_LIMIT_BUNDLE_KEY, resultsLimit)); + if (inState.containsKey(SEARCH_TEXT_BUNDLE_KEY)) { + setSearchText(inState.getString(SEARCH_TEXT_BUNDLE_KEY)); + } + if (inState.containsKey(LOCATION_BUNDLE_KEY)) { + Location location = inState.getParcelable(LOCATION_BUNDLE_KEY); + setLocation(location); + } + showSearchBox = inState.getBoolean(SHOW_SEARCH_BOX_BUNDLE_KEY, showSearchBox); + } + } + + private Timer createSearchTextTimer() { + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + onSearchTextTimerTriggered(); + } + }, 0, searchTextTimerDelayInMilliseconds); + + return timer; + } + + private void onSearchTextTimerTriggered() { + if (hasSearchTextChangedSinceLastQuery) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + FacebookException error = null; + try { + loadData(true); + } catch (FacebookException fe) { + error = fe; + } catch (Exception e) { + error = new FacebookException(e); + } finally { + if (error != null) { + OnErrorListener onErrorListener = getOnErrorListener(); + if (onErrorListener != null) { + onErrorListener.onError(PlacePickerFragment.this, error); + } else { + Logger.log(LoggingBehavior.REQUESTS, TAG, "Error loading data : %s", error); + } + } + } + } + }); + } else { + // Nothing has changed in 2 seconds. Invalidate and forget about this timer. + // Next time the user types, we will fire a query immediately again. + searchTextTimer.cancel(); + searchTextTimer = null; + } + } + + private class AsNeededLoadingStrategy extends LoadingStrategy { + @Override + public void attach(GraphObjectAdapter adapter) { + super.attach(adapter); + + this.adapter.setDataNeededListener(new GraphObjectAdapter.DataNeededListener() { + @Override + public void onDataNeeded() { + // Do nothing if we are currently loading data . We will get notified again when that load finishes if the adapter still + // needs more data. Otherwise, follow the next link. + if (!loader.isLoading()) { + loader.followNextLink(); + } + } + }); + } + + @Override + protected void onLoadFinished(GraphObjectPagingLoader loader, + SimpleGraphObjectCursor data) { + super.onLoadFinished(loader, data); + + // We could be called in this state if we are clearing data or if we are being re-attached + // in the middle of a query. + if (data == null || loader.isLoading()) { + return; + } + + hideActivityCircle(); + + if (data.isFromCache()) { + // Only the first page can be cached, since all subsequent pages will be round-tripped. Force + // a refresh of the first page before we allow paging to begin. If the first page produced + // no data, launch the refresh immediately, otherwise schedule it for later. + loader.refreshOriginalRequest(data.areMoreObjectsAvailable() ? CACHED_RESULT_REFRESH_DELAY : 0); + } + } + } + + private class SearchTextWatcher implements TextWatcher { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + onSearchBoxTextChanged(s.toString(), false); + } + + @Override + public void afterTextChanged(Editable s) { + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/ProfilePictureView.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/ProfilePictureView.java new file mode 100644 index 0000000000..fdc87288ba --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/ProfilePictureView.java @@ -0,0 +1,538 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.facebook.FacebookException; +import com.facebook.LoggingBehavior; +import com.facebook.android.R; +import com.facebook.internal.*; + +import java.net.URISyntaxException; + +/** + * View that displays the profile photo of a supplied profile ID, while conforming + * to user specified dimensions. + */ +public class ProfilePictureView extends FrameLayout { + + /** + * Callback interface that will be called when a network or other error is encountered + * while retrieving profile pictures. + */ + public interface OnErrorListener { + /** + * Called when a network or other error is encountered. + * @param error a FacebookException representing the error that was encountered. + */ + void onError(FacebookException error); + } + + /** + * Tag used when logging calls are made by ProfilePictureView + */ + public static final String TAG = ProfilePictureView.class.getSimpleName(); + + /** + * Indicates that the specific size of the View will be set via layout params. + * ProfilePictureView will default to NORMAL X NORMAL, if the layout params set on + * this instance do not have a fixed size. + * Used in calls to setPresetSize() and getPresetSize(). + * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView. + */ + public static final int CUSTOM = -1; + + /** + * Indicates that the profile image should fit in a SMALL X SMALL space, regardless + * of whether the cropped or un-cropped version is chosen. + * Used in calls to setPresetSize() and getPresetSize(). + * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView. + */ + public static final int SMALL = -2; + + /** + * Indicates that the profile image should fit in a NORMAL X NORMAL space, regardless + * of whether the cropped or un-cropped version is chosen. + * Used in calls to setPresetSize() and getPresetSize(). + * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView. + */ + public static final int NORMAL = -3; + + /** + * Indicates that the profile image should fit in a LARGE X LARGE space, regardless + * of whether the cropped or un-cropped version is chosen. + * Used in calls to setPresetSize() and getPresetSize(). + * Corresponds with the preset_size Xml attribute that can be set on ProfilePictureView. + */ + public static final int LARGE = -4; + + private static final int MIN_SIZE = 1; + private static final boolean IS_CROPPED_DEFAULT_VALUE = true; + private static final String SUPER_STATE_KEY = "ProfilePictureView_superState"; + private static final String PROFILE_ID_KEY = "ProfilePictureView_profileId"; + private static final String PRESET_SIZE_KEY = "ProfilePictureView_presetSize"; + private static final String IS_CROPPED_KEY = "ProfilePictureView_isCropped"; + private static final String BITMAP_KEY = "ProfilePictureView_bitmap"; + private static final String BITMAP_WIDTH_KEY = "ProfilePictureView_width"; + private static final String BITMAP_HEIGHT_KEY = "ProfilePictureView_height"; + private static final String PENDING_REFRESH_KEY = "ProfilePictureView_refresh"; + + private String profileId; + private int queryHeight = ImageRequest.UNSPECIFIED_DIMENSION; + private int queryWidth = ImageRequest.UNSPECIFIED_DIMENSION; + private boolean isCropped = IS_CROPPED_DEFAULT_VALUE; + private Bitmap imageContents; + private ImageView image; + private int presetSizeType = CUSTOM; + private ImageRequest lastRequest; + private OnErrorListener onErrorListener; + private Bitmap customizedDefaultProfilePicture = null; + + /** + * Constructor + * + * @param context Context for this View + */ + public ProfilePictureView(Context context) { + super(context); + initialize(context); + } + + /** + * Constructor + * + * @param context Context for this View + * @param attrs AttributeSet for this View. + * The attribute 'preset_size' is processed here + */ + public ProfilePictureView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(context); + parseAttributes(attrs); + } + + /** + * Constructor + * + * @param context Context for this View + * @param attrs AttributeSet for this View. + * The attribute 'preset_size' is processed here + * @param defStyle Default style for this View + */ + public ProfilePictureView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initialize(context); + parseAttributes(attrs); + } + + /** + * Gets the current preset size type + * + * @return The current preset size type, if set; CUSTOM if not + */ + public final int getPresetSize() { + return presetSizeType; + } + + /** + * Apply a preset size to this profile photo + * + * @param sizeType The size type to apply: SMALL, NORMAL or LARGE + */ + public final void setPresetSize(int sizeType) { + switch (sizeType) { + case SMALL: + case NORMAL: + case LARGE: + case CUSTOM: + this.presetSizeType = sizeType; + break; + + default: + throw new IllegalArgumentException("Must use a predefined preset size"); + } + + requestLayout(); + } + + /** + * Indicates whether the cropped version of the profile photo has been chosen + * + * @return True if the cropped version is chosen, false if not. + */ + public final boolean isCropped() { + return isCropped; + } + + /** + * Sets the profile photo to be the cropped version, or the original version + * + * @param showCroppedVersion True to select the cropped version + * False to select the standard version + */ + public final void setCropped(boolean showCroppedVersion) { + isCropped = showCroppedVersion; + // No need to force the refresh since we will catch the change in required dimensions + refreshImage(false); + } + + /** + * Returns the profile Id for the current profile photo + * + * @return The profile Id + */ + public final String getProfileId() { + return profileId; + } + + /** + * Sets the profile Id for this profile photo + * + * @param profileId The profileId + * NULL/Empty String will show the blank profile photo + */ + public final void setProfileId(String profileId) { + boolean force = false; + if (Utility.isNullOrEmpty(this.profileId) || !this.profileId.equalsIgnoreCase(profileId)) { + // Clear out the old profilePicture before requesting for the new one. + setBlankProfilePicture(); + force = true; + } + + this.profileId = profileId; + refreshImage(force); + } + + /** + * Returns the current OnErrorListener for this instance of ProfilePictureView + * + * @return The OnErrorListener + */ + public final OnErrorListener getOnErrorListener() { + return onErrorListener; + } + + /** + * Sets an OnErrorListener for this instance of ProfilePictureView to call into when + * certain exceptions occur. + * + * @param onErrorListener The Listener object to set + */ + public final void setOnErrorListener(OnErrorListener onErrorListener) { + this.onErrorListener = onErrorListener; + } + + /** + * The ProfilePictureView will display the provided image while the specified + * profile is being loaded, or if the specified profile is not available. + * + * @param inputBitmap The bitmap to render until the actual profile is loaded. + */ + public final void setDefaultProfilePicture(Bitmap inputBitmap) { + customizedDefaultProfilePicture = inputBitmap; + } + + /** + * Overriding onMeasure to handle the case where WRAP_CONTENT might be + * specified in the layout. Since we don't know the dimensions of the profile + * photo, we need to handle this case specifically. + *

+ * The approach is to default to a NORMAL sized amount of space in the case that + * a preset size is not specified. This logic is applied to both width and height + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + ViewGroup.LayoutParams params = getLayoutParams(); + boolean customMeasure = false; + int newHeight = MeasureSpec.getSize(heightMeasureSpec); + int newWidth = MeasureSpec.getSize(widthMeasureSpec); + if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY && + params.height == ViewGroup.LayoutParams.WRAP_CONTENT) { + newHeight = getPresetSizeInPixels(true); // Default to a preset size + heightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY); + customMeasure = true; + } + + if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY && + params.width == ViewGroup.LayoutParams.WRAP_CONTENT) { + newWidth = getPresetSizeInPixels(true); // Default to a preset size + widthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY); + customMeasure = true; + } + + if (customMeasure) { + // Since we are providing custom dimensions, we need to handle the measure + // phase from here + setMeasuredDimension(newWidth, newHeight); + measureChildren(widthMeasureSpec, heightMeasureSpec); + } else { + // Rely on FrameLayout to do the right thing + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + /** + * In addition to calling super.Layout(), we also attempt to get a new image that + * is properly size for the layout dimensions + */ + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + // See if the image needs redrawing + refreshImage(false); + } + + /** + * Some of the current state is returned as a Bundle to allow quick restoration + * of the ProfilePictureView object in scenarios like orientation changes. + * @return a Parcelable containing the current state + */ + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + Bundle instanceState = new Bundle(); + instanceState.putParcelable(SUPER_STATE_KEY, superState); + instanceState.putString(PROFILE_ID_KEY, profileId); + instanceState.putInt(PRESET_SIZE_KEY, presetSizeType); + instanceState.putBoolean(IS_CROPPED_KEY, isCropped); + instanceState.putParcelable(BITMAP_KEY, imageContents); + instanceState.putInt(BITMAP_WIDTH_KEY, queryWidth); + instanceState.putInt(BITMAP_HEIGHT_KEY, queryHeight); + instanceState.putBoolean(PENDING_REFRESH_KEY, lastRequest != null); + + return instanceState; + } + + /** + * If the passed in state is a Bundle, an attempt is made to restore from it. + * @param state a Parcelable containing the current state + */ + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state.getClass() != Bundle.class) { + super.onRestoreInstanceState(state); + } else { + Bundle instanceState = (Bundle)state; + super.onRestoreInstanceState(instanceState.getParcelable(SUPER_STATE_KEY)); + + profileId = instanceState.getString(PROFILE_ID_KEY); + presetSizeType = instanceState.getInt(PRESET_SIZE_KEY); + isCropped = instanceState.getBoolean(IS_CROPPED_KEY); + queryWidth = instanceState.getInt(BITMAP_WIDTH_KEY); + queryHeight = instanceState.getInt(BITMAP_HEIGHT_KEY); + + setImageBitmap((Bitmap)instanceState.getParcelable(BITMAP_KEY)); + + if (instanceState.getBoolean(PENDING_REFRESH_KEY)) { + refreshImage(true); + } + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + // Null out lastRequest. This way, when the response is returned, we can ascertain + // that the view is detached and hence should not attempt to update its contents. + lastRequest = null; + } + + private void initialize(Context context) { + // We only want our ImageView in here. Nothing else is permitted + removeAllViews(); + + image = new ImageView(context); + + LayoutParams imageLayout = new LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + + image.setLayoutParams(imageLayout); + + // We want to prevent up-scaling the image, but still have it fit within + // the layout bounds as best as possible. + image.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + addView(image); + } + + private void parseAttributes(AttributeSet attrs) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_profile_picture_view); + setPresetSize(a.getInt(R.styleable.com_facebook_profile_picture_view_preset_size, CUSTOM)); + isCropped = a.getBoolean(R.styleable.com_facebook_profile_picture_view_is_cropped, IS_CROPPED_DEFAULT_VALUE); + a.recycle(); + } + + private void refreshImage(boolean force) { + boolean changed = updateImageQueryParameters(); + // Note: do not use Utility.isNullOrEmpty here as this will cause the Eclipse + // Graphical Layout editor to fail in some cases + if (profileId == null || profileId.length() == 0 || + ((queryWidth == ImageRequest.UNSPECIFIED_DIMENSION) && + (queryHeight == ImageRequest.UNSPECIFIED_DIMENSION))) { + setBlankProfilePicture(); + } else if (changed || force) { + sendImageRequest(true); + } + } + + private void setBlankProfilePicture() { + if (customizedDefaultProfilePicture == null) { + int blankImageResource = isCropped() ? + R.drawable.com_facebook_profile_picture_blank_square : + R.drawable.com_facebook_profile_picture_blank_portrait; + setImageBitmap( BitmapFactory.decodeResource(getResources(), blankImageResource)); + } else { + // Update profile image dimensions. + updateImageQueryParameters(); + // Resize inputBitmap to new dimensions of queryWidth and queryHeight. + Bitmap scaledBitmap = Bitmap.createScaledBitmap(customizedDefaultProfilePicture, queryWidth, queryHeight, false); + setImageBitmap(scaledBitmap); + } + } + + private void setImageBitmap(Bitmap imageBitmap) { + if (image != null && imageBitmap != null) { + imageContents = imageBitmap; // Hold for save-restore cycles + image.setImageBitmap(imageBitmap); + } + } + + private void sendImageRequest(boolean allowCachedResponse) { + try { + ImageRequest.Builder requestBuilder = new ImageRequest.Builder( + getContext(), + ImageRequest.getProfilePictureUrl(profileId, queryWidth, queryHeight)); + + ImageRequest request = requestBuilder.setAllowCachedRedirects(allowCachedResponse) + .setCallerTag(this) + .setCallback( + new ImageRequest.Callback() { + @Override + public void onCompleted(ImageResponse response) { + processResponse(response); + } + }) + .build(); + + // Make sure to cancel the old request before sending the new one to prevent + // accidental cancellation of the new request. This could happen if the URL and + // caller tag stayed the same. + if (lastRequest != null) { + ImageDownloader.cancelRequest(lastRequest); + } + lastRequest = request; + + ImageDownloader.downloadAsync(request); + } catch (URISyntaxException e) { + Logger.log(LoggingBehavior.REQUESTS, Log.ERROR, TAG, e.toString()); + } + } + + private void processResponse(ImageResponse response) { + // First check if the response is for the right request. We may have: + // 1. Sent a new request, thus super-ceding this one. + // 2. Detached this view, in which case the response should be discarded. + if (response.getRequest() == lastRequest) { + lastRequest = null; + Bitmap responseImage = response.getBitmap(); + Exception error = response.getError(); + if (error != null) { + OnErrorListener listener = onErrorListener; + if (listener != null) { + listener.onError(new FacebookException( + "Error in downloading profile picture for profileId: " + getProfileId(), error)); + } else { + Logger.log(LoggingBehavior.REQUESTS, Log.ERROR, TAG, error.toString()); + } + } else if (responseImage != null) { + setImageBitmap(responseImage); + + if (response.isCachedRedirect()) { + sendImageRequest(false); + } + } + } + } + + private boolean updateImageQueryParameters() { + int newHeightPx = getHeight(); + int newWidthPx = getWidth(); + if (newWidthPx < MIN_SIZE || newHeightPx < MIN_SIZE) { + // Not enough space laid out for this View yet. Or something else is awry. + return false; + } + + int presetSize = getPresetSizeInPixels(false); + if (presetSize != ImageRequest.UNSPECIFIED_DIMENSION) { + newWidthPx = presetSize; + newHeightPx = presetSize; + } + + // The cropped version is square + // If full version is desired, then only one dimension is required. + if (newWidthPx <= newHeightPx) { + newHeightPx = isCropped() ? newWidthPx : ImageRequest.UNSPECIFIED_DIMENSION; + } else { + newWidthPx = isCropped() ? newHeightPx : ImageRequest.UNSPECIFIED_DIMENSION; + } + + boolean changed = (newWidthPx != queryWidth) || (newHeightPx != queryHeight); + + queryWidth = newWidthPx; + queryHeight = newHeightPx; + + return changed; + } + + private int getPresetSizeInPixels(boolean forcePreset) { + int dimensionId; + switch (presetSizeType) { + case SMALL: + dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_small; + break; + case NORMAL: + dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_normal; + break; + case LARGE: + dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_large; + break; + case CUSTOM: + if (!forcePreset) { + return ImageRequest.UNSPECIFIED_DIMENSION; + } else { + dimensionId = R.dimen.com_facebook_profilepictureview_preset_size_normal; + break; + } + default: + return ImageRequest.UNSPECIFIED_DIMENSION; + } + + return getResources().getDimensionPixelSize(dimensionId); + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/SimpleGraphObjectCursor.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/SimpleGraphObjectCursor.java new file mode 100644 index 0000000000..04c98ba6df --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/SimpleGraphObjectCursor.java @@ -0,0 +1,162 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.database.CursorIndexOutOfBoundsException; +import com.facebook.model.GraphObject; + +import java.util.ArrayList; +import java.util.Collection; + +class SimpleGraphObjectCursor implements GraphObjectCursor { + private int pos = -1; + private boolean closed = false; + private ArrayList graphObjects = new ArrayList(); + private boolean moreObjectsAvailable = false; + private boolean fromCache = false; + + SimpleGraphObjectCursor() { + } + + SimpleGraphObjectCursor(SimpleGraphObjectCursor other) { + pos = other.pos; + closed = other.closed; + graphObjects = new ArrayList(); + graphObjects.addAll(other.graphObjects); + fromCache = other.fromCache; + + // We do not copy observers. + } + + public void addGraphObjects(Collection graphObjects, boolean fromCache) { + this.graphObjects.addAll(graphObjects); + // We consider this cached if ANY results were from the cache. + this.fromCache |= fromCache; + } + + public boolean isFromCache() { + return fromCache; + } + + public void setFromCache(boolean fromCache) { + this.fromCache = fromCache; + } + + public boolean areMoreObjectsAvailable() { + return moreObjectsAvailable; + } + + public void setMoreObjectsAvailable(boolean moreObjectsAvailable) { + this.moreObjectsAvailable = moreObjectsAvailable; + } + + @Override + public int getCount() { + return graphObjects.size(); + } + + @Override + public int getPosition() { + return pos; + } + + @Override + public boolean move(int offset) { + return moveToPosition(pos + offset); + } + + @Override + public boolean moveToPosition(int position) { + final int count = getCount(); + if (position >= count) { + pos = count; + return false; + } + + if (position < 0) { + pos = -1; + return false; + } + + pos = position; + return true; + } + + @Override + public boolean moveToFirst() { + return moveToPosition(0); + } + + @Override + public boolean moveToLast() { + return moveToPosition(getCount() - 1); + } + + @Override + public boolean moveToNext() { + return moveToPosition(pos + 1); + } + + @Override + public boolean moveToPrevious() { + return moveToPosition(pos - 1); + } + + @Override + public boolean isFirst() { + return (pos == 0) && (getCount() != 0); + } + + @Override + public boolean isLast() { + final int count = getCount(); + return (pos == (count - 1)) && (count != 0); + } + + @Override + public boolean isBeforeFirst() { + return (getCount() == 0) || (pos == -1); + } + + @Override + public boolean isAfterLast() { + final int count = getCount(); + return (count == 0) || (pos == count); + } + + @Override + public T getGraphObject() { + if (pos < 0) { + throw new CursorIndexOutOfBoundsException("Before first object."); + } + if (pos >= graphObjects.size()) { + throw new CursorIndexOutOfBoundsException("After last object."); + } + return graphObjects.get(pos); + } + + @Override + public void close() { + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } + +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/ToolTipPopup.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/ToolTipPopup.java new file mode 100644 index 0000000000..ab2ef1b2fd --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/ToolTipPopup.java @@ -0,0 +1,236 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import java.lang.ref.WeakReference; +import com.facebook.android.R; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.Window; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.PopupWindow; +import android.widget.TextView; + +public class ToolTipPopup { + + public static enum Style { + /** + * The tool tip will be shown with a blue style; including a blue background and blue + * arrows. + */ + BLUE, + + /** + * The tool tip will be shown with a black style; including a black background and black + * arrows. + */ + BLACK + } + + /** + * The default time that the tool tip will be displayed + */ + public static final long DEFAULT_POPUP_DISPLAY_TIME = 6000; + + private final String mText; + private final WeakReference mAnchorViewRef; + private final Context mContext; + private PopupContentView mPopupContent; + private PopupWindow mPopupWindow; + private Style mStyle = Style.BLUE; + private long mNuxDisplayTime = DEFAULT_POPUP_DISPLAY_TIME; + + private final ViewTreeObserver.OnScrollChangedListener mScrollListener = + new ViewTreeObserver.OnScrollChangedListener() { + @Override + public void onScrollChanged() { + if (mAnchorViewRef.get() != null && + mPopupWindow != null && + mPopupWindow.isShowing()) { + if (mPopupWindow.isAboveAnchor()) { + mPopupContent.showBottomArrow(); + } else { + mPopupContent.showTopArrow(); + } + } + } + }; + + /** + * Create a new ToolTipPopup + * @param text The text to be displayed in the tool tip + * @param anchor The view to anchor this tool tip to. + */ + public ToolTipPopup(String text, View anchor) { + mText = text; + mAnchorViewRef = new WeakReference(anchor); + mContext = anchor.getContext(); + } + + /** + * Sets the {@link Style} of this tool tip. + * @param mStyle + */ + public void setStyle(Style mStyle) { + this.mStyle = mStyle; + } + + /** + * Display this tool tip to the user + */ + @SuppressLint("WrongCall") + public void show() { + if (mAnchorViewRef.get() != null) { + mPopupContent = new PopupContentView(mContext); + TextView body = (TextView) mPopupContent.findViewById( + R.id.com_facebook_tooltip_bubble_view_text_body); + body.setText(mText); + if (mStyle == Style.BLUE) { + mPopupContent.bodyFrame.setBackgroundResource( + R.drawable.com_facebook_tooltip_blue_background); + mPopupContent.bottomArrow.setImageResource( + R.drawable.com_facebook_tooltip_blue_bottomnub); + mPopupContent.topArrow.setImageResource( + R.drawable.com_facebook_tooltip_blue_topnub); + mPopupContent.xOut.setImageResource(R.drawable.com_facebook_tooltip_blue_xout); + } else { + mPopupContent.bodyFrame.setBackgroundResource( + R.drawable.com_facebook_tooltip_black_background); + mPopupContent.bottomArrow.setImageResource( + R.drawable.com_facebook_tooltip_black_bottomnub); + mPopupContent.topArrow.setImageResource( + R.drawable.com_facebook_tooltip_black_topnub); + mPopupContent.xOut.setImageResource(R.drawable.com_facebook_tooltip_black_xout); + } + + final Window window = ((Activity) mContext).getWindow(); + final View decorView = window.getDecorView(); + final int decorWidth = decorView.getWidth(); + final int decorHeight = decorView.getHeight(); + registerObserver(); + mPopupContent.onMeasure( + View.MeasureSpec.makeMeasureSpec(decorWidth, View.MeasureSpec.AT_MOST), + View.MeasureSpec.makeMeasureSpec(decorHeight, View.MeasureSpec.AT_MOST)); + mPopupWindow = new PopupWindow( + mPopupContent, + mPopupContent.getMeasuredWidth(), + mPopupContent.getMeasuredHeight()); + mPopupWindow.showAsDropDown(mAnchorViewRef.get()); + updateArrows(); + if (mNuxDisplayTime > 0) { + mPopupContent.postDelayed(new Runnable() { + @Override + public void run() { + dismiss(); + } + }, mNuxDisplayTime); + } + mPopupWindow.setTouchable(true); + mPopupContent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + } + } + + /** + * Set the time (in milliseconds) the tool tip will be displayed. Any number less than or equal + * to 0 will cause the tool tip to be displayed indefinitely + * @param displayTime The amount of time (in milliseconds) to display the tool tip + */ + public void setNuxDisplayTime(long displayTime) { + this.mNuxDisplayTime = displayTime; + } + + private void updateArrows() { + if (mPopupWindow != null && mPopupWindow.isShowing()) { + if (mPopupWindow.isAboveAnchor()) { + mPopupContent.showBottomArrow(); + } else { + mPopupContent.showTopArrow(); + } + } + } + + /** + * Dismiss the tool tip + */ + public void dismiss() { + unregisterObserver(); + if (mPopupWindow != null) { + mPopupWindow.dismiss(); + } + } + + private void registerObserver() { + unregisterObserver(); + if (mAnchorViewRef.get() != null) { + mAnchorViewRef.get().getViewTreeObserver().addOnScrollChangedListener(mScrollListener); + } + } + + private void unregisterObserver() { + if (mAnchorViewRef.get() != null) { + mAnchorViewRef.get().getViewTreeObserver().removeOnScrollChangedListener(mScrollListener); + } + } + + private class PopupContentView extends FrameLayout { + private ImageView topArrow; + private ImageView bottomArrow; + private View bodyFrame; + private ImageView xOut; + + public PopupContentView(Context context) { + super(context); + init(); + } + + private void init() { + LayoutInflater inflater = LayoutInflater.from(getContext()); + inflater.inflate(R.layout.com_facebook_tooltip_bubble, this); + topArrow = (ImageView) findViewById(R.id.com_facebook_tooltip_bubble_view_top_pointer); + bottomArrow = (ImageView) findViewById( + R.id.com_facebook_tooltip_bubble_view_bottom_pointer); + bodyFrame = findViewById(R.id.com_facebook_body_frame); + xOut = (ImageView) findViewById(R.id.com_facebook_button_xout); + } + + public void showTopArrow() { + topArrow.setVisibility(View.VISIBLE); + bottomArrow.setVisibility(View.INVISIBLE); + } + + public void showBottomArrow() { + topArrow.setVisibility(View.INVISIBLE); + bottomArrow.setVisibility(View.VISIBLE); + } + + // Expose so popup content can be sized + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/UserSettingsFragment.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/UserSettingsFragment.java new file mode 100644 index 0000000000..ed6cc9ceb5 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/UserSettingsFragment.java @@ -0,0 +1,452 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.facebook.*; +import com.facebook.android.R; +import com.facebook.internal.AnalyticsEvents; +import com.facebook.internal.ImageDownloader; +import com.facebook.internal.ImageRequest; +import com.facebook.internal.ImageResponse; +import com.facebook.model.GraphUser; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +/** + * A Fragment that displays a Login/Logout button as well as the user's + * profile picture and name when logged in. + *

+ * This Fragment will create and use the active session upon construction + * if it has the available data (if the app ID is specified in the manifest). + * It will also open the active session if it does not require user interaction + * (i.e. if the session is in the {@link com.facebook.SessionState#CREATED_TOKEN_LOADED} state. + * Developers can override the use of the active session by calling + * the {@link #setSession(com.facebook.Session)} method. + */ +public class UserSettingsFragment extends FacebookFragment { + + private static final String NAME = "name"; + private static final String ID = "id"; + private static final String PICTURE = "picture"; + private static final String FIELDS = "fields"; + + private static final String REQUEST_FIELDS = TextUtils.join(",", new String[] {ID, NAME, PICTURE}); + + private LoginButton loginButton; + private LoginButton.LoginButtonProperties loginButtonProperties = new LoginButton.LoginButtonProperties(); + private TextView connectedStateLabel; + private GraphUser user; + private Session userInfoSession; // the Session used to fetch the current user info + private Drawable userProfilePic; + private String userProfilePicID; + private Session.StatusCallback sessionStatusCallback; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.com_facebook_usersettingsfragment, container, false); + loginButton = (LoginButton) view.findViewById(R.id.com_facebook_usersettingsfragment_login_button); + loginButton.setProperties(loginButtonProperties); + loginButton.setFragment(this); + loginButton.setLoginLogoutEventName(AnalyticsEvents.EVENT_USER_SETTINGS_USAGE); + + Session session = getSession(); + if (session != null && !session.equals(Session.getActiveSession())) { + loginButton.setSession(session); + } + connectedStateLabel = (TextView) view.findViewById(R.id.com_facebook_usersettingsfragment_profile_name); + + // if no background is set for some reason, then default to Facebook blue + if (view.getBackground() == null) { + view.setBackgroundColor(getResources().getColor(R.color.com_facebook_blue)); + } else { + view.getBackground().setDither(true); + } + return view; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + /** + * @throws com.facebook.FacebookException if errors occur during the loading of user information + */ + @Override + public void onResume() { + super.onResume(); + fetchUserInfo(); + updateUI(); + } + + /** + * Set the Session object to use instead of the active Session. Since a Session + * cannot be reused, if the user logs out from this Session, and tries to + * log in again, a new Active Session will be used instead. + *

+ * If the passed in session is currently opened, this method will also attempt to + * load some user information for display (if needed). + * + * @param newSession the Session object to use + * @throws com.facebook.FacebookException if errors occur during the loading of user information + */ + @Override + public void setSession(Session newSession) { + super.setSession(newSession); + if (loginButton != null) { + loginButton.setSession(newSession); + } + fetchUserInfo(); + updateUI(); + } + + /** + * Sets the default audience to use when the session is opened. + * This value is only useful when specifying write permissions for the native + * login dialog. + * + * @param defaultAudience the default audience value to use + */ + public void setDefaultAudience(SessionDefaultAudience defaultAudience) { + loginButtonProperties.setDefaultAudience(defaultAudience); + } + + /** + * Gets the default audience to use when the session is opened. + * This value is only useful when specifying write permissions for the native + * login dialog. + * + * @return the default audience value to use + */ + public SessionDefaultAudience getDefaultAudience() { + return loginButtonProperties.getDefaultAudience(); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * can only be read permissions. If any publish permissions are included, the login + * attempt by the user will fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the UserSettingsFragment is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setPublishPermissions has been called + */ + public void setReadPermissions(List permissions) { + loginButtonProperties.setReadPermissions(permissions, getSession()); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * can only be read permissions. If any publish permissions are included, the login + * attempt by the user will fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the UserSettingsFragment is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setPublishPermissions has been called + */ + public void setReadPermissions(String... permissions) { + loginButtonProperties.setReadPermissions(Arrays.asList(permissions), getSession()); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * should only be publish permissions. If any read permissions are included, the login + * attempt by the user may fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the LoginButton is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setReadPermissions has been called + * @throws IllegalArgumentException if permissions is null or empty + */ + public void setPublishPermissions(List permissions) { + loginButtonProperties.setPublishPermissions(permissions, getSession()); + } + + /** + * Set the permissions to use when the session is opened. The permissions here + * should only be publish permissions. If any read permissions are included, the login + * attempt by the user may fail. The LoginButton can only be associated with either + * read permissions or publish permissions, but not both. Calling both + * setReadPermissions and setPublishPermissions on the same instance of LoginButton + * will result in an exception being thrown unless clearPermissions is called in between. + *

+ * This method is only meaningful if called before the session is open. If this is called + * after the session is opened, and the list of permissions passed in is not a subset + * of the permissions granted during the authorization, it will log an error. + *

+ * Since the session can be automatically opened when the LoginButton is constructed, + * it's important to always pass in a consistent set of permissions to this method, or + * manage the setting of permissions outside of the LoginButton class altogether + * (by managing the session explicitly). + * + * @param permissions the read permissions to use + * + * @throws UnsupportedOperationException if setReadPermissions has been called + * @throws IllegalArgumentException if permissions is null or empty + */ + public void setPublishPermissions(String... permissions) { + loginButtonProperties.setPublishPermissions(Arrays.asList(permissions), getSession()); + } + + + /** + * Clears the permissions currently associated with this LoginButton. + */ + public void clearPermissions() { + loginButtonProperties.clearPermissions(); + } + + /** + * Sets the login behavior for the session that will be opened. If null is specified, + * the default ({@link SessionLoginBehavior SessionLoginBehavior.SSO_WITH_FALLBACK} + * will be used. + * + * @param loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + */ + public void setLoginBehavior(SessionLoginBehavior loginBehavior) { + loginButtonProperties.setLoginBehavior(loginBehavior); + } + + /** + * Gets the login behavior for the session that will be opened. If null is returned, + * the default ({@link SessionLoginBehavior SessionLoginBehavior.SSO_WITH_FALLBACK} + * will be used. + * + * @return loginBehavior The {@link SessionLoginBehavior SessionLoginBehavior} that + * specifies what behaviors should be attempted during + * authorization. + */ + public SessionLoginBehavior getLoginBehavior() { + return loginButtonProperties.getLoginBehavior(); + } + + /** + * Sets an OnErrorListener for this instance of UserSettingsFragment to call into when + * certain exceptions occur. + * + * @param onErrorListener The listener object to set + */ + public void setOnErrorListener(LoginButton.OnErrorListener onErrorListener) { + loginButtonProperties.setOnErrorListener(onErrorListener); + } + + /** + * Returns the current OnErrorListener for this instance of UserSettingsFragment. + * + * @return The OnErrorListener + */ + public LoginButton.OnErrorListener getOnErrorListener() { + return loginButtonProperties.getOnErrorListener(); + } + + /** + * Sets the callback interface that will be called whenever the status of the Session + * associated with this LoginButton changes. + * + * @param callback the callback interface + */ + public void setSessionStatusCallback(Session.StatusCallback callback) { + this.sessionStatusCallback = callback; + } + + /** + * Sets the callback interface that will be called whenever the status of the Session + * associated with this LoginButton changes. + + * @return the callback interface + */ + public Session.StatusCallback getSessionStatusCallback() { + return sessionStatusCallback; + } + + @Override + protected void onSessionStateChange(SessionState state, Exception exception) { + fetchUserInfo(); + updateUI(); + + if (sessionStatusCallback != null) { + sessionStatusCallback.call(getSession(), state, exception); + } + } + + // For Testing Only + List getPermissions() { + return loginButtonProperties.getPermissions(); + } + + private void fetchUserInfo() { + final Session currentSession = getSession(); + if (currentSession != null && currentSession.isOpened()) { + if (currentSession != userInfoSession) { + Request request = Request.newMeRequest(currentSession, new Request.GraphUserCallback() { + @Override + public void onCompleted(GraphUser me, Response response) { + if (currentSession == getSession()) { + user = me; + updateUI(); + } + if (response.getError() != null) { + loginButton.handleError(response.getError().getException()); + } + } + }); + Bundle parameters = new Bundle(); + parameters.putString(FIELDS, REQUEST_FIELDS); + request.setParameters(parameters); + Request.executeBatchAsync(request); + userInfoSession = currentSession; + } + } else { + user = null; + } + } + + private void updateUI() { + if (!isAdded()) { + return; + } + if (isSessionOpen()) { + connectedStateLabel.setTextColor(getResources().getColor(R.color.com_facebook_usersettingsfragment_connected_text_color)); + connectedStateLabel.setShadowLayer(1f, 0f, -1f, + getResources().getColor(R.color.com_facebook_usersettingsfragment_connected_shadow_color)); + + if (user != null) { + ImageRequest request = getImageRequest(); + if (request != null) { + URI requestUrl = request.getImageUri(); + // Do we already have the right picture? If so, leave it alone. + if (!requestUrl.equals(connectedStateLabel.getTag())) { + if (user.getId().equals(userProfilePicID)) { + connectedStateLabel.setCompoundDrawables(null, userProfilePic, null, null); + connectedStateLabel.setTag(requestUrl); + } else { + ImageDownloader.downloadAsync(request); + } + } + } + connectedStateLabel.setText(user.getName()); + } else { + connectedStateLabel.setText(getResources().getString( + R.string.com_facebook_usersettingsfragment_logged_in)); + Drawable noProfilePic = getResources().getDrawable(R.drawable.com_facebook_profile_default_icon); + noProfilePic.setBounds(0, 0, + getResources().getDimensionPixelSize(R.dimen.com_facebook_usersettingsfragment_profile_picture_width), + getResources().getDimensionPixelSize(R.dimen.com_facebook_usersettingsfragment_profile_picture_height)); + connectedStateLabel.setCompoundDrawables(null, noProfilePic, null, null); + } + } else { + int textColor = getResources().getColor(R.color.com_facebook_usersettingsfragment_not_connected_text_color); + connectedStateLabel.setTextColor(textColor); + connectedStateLabel.setShadowLayer(0f, 0f, 0f, textColor); + connectedStateLabel.setText(getResources().getString( + R.string.com_facebook_usersettingsfragment_not_logged_in)); + connectedStateLabel.setCompoundDrawables(null, null, null, null); + connectedStateLabel.setTag(null); + } + } + + private ImageRequest getImageRequest() { + ImageRequest request = null; + try { + ImageRequest.Builder requestBuilder = new ImageRequest.Builder( + getActivity(), + ImageRequest.getProfilePictureUrl( + user.getId(), + getResources().getDimensionPixelSize(R.dimen.com_facebook_usersettingsfragment_profile_picture_width), + getResources().getDimensionPixelSize(R.dimen.com_facebook_usersettingsfragment_profile_picture_height))); + + request = requestBuilder.setCallerTag(this) + .setCallback( + new ImageRequest.Callback() { + @Override + public void onCompleted(ImageResponse response) { + processImageResponse(user.getId(), response); + } + }) + .build(); + } catch (URISyntaxException e) { + } + return request; + } + + private void processImageResponse(String id, ImageResponse response) { + if (response != null) { + Bitmap bitmap = response.getBitmap(); + if (bitmap != null) { + BitmapDrawable drawable = new BitmapDrawable(UserSettingsFragment.this.getResources(), bitmap); + drawable.setBounds(0, 0, + getResources().getDimensionPixelSize(R.dimen.com_facebook_usersettingsfragment_profile_picture_width), + getResources().getDimensionPixelSize(R.dimen.com_facebook_usersettingsfragment_profile_picture_height)); + userProfilePic = drawable; + userProfilePicID = id; + connectedStateLabel.setCompoundDrawables(null, drawable, null, null); + connectedStateLabel.setTag(response.getRequest().getImageUri()); + } + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/WebDialog.java b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/WebDialog.java new file mode 100644 index 0000000000..84be24eec5 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/java/com/facebook/widget/WebDialog.java @@ -0,0 +1,894 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook.widget; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.view.*; +import android.webkit.SslErrorHandler; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import com.facebook.*; +import com.facebook.android.*; +import com.facebook.internal.Logger; +import com.facebook.internal.ServerProtocol; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; + +/** + * This class provides a mechanism for displaying Facebook Web dialogs inside a Dialog. Helper + * methods are provided to construct commonly-used dialogs, or a caller can specify arbitrary + * parameters to call other dialogs. + */ +public class WebDialog extends Dialog { + private static final String LOG_TAG = Logger.LOG_TAG_BASE + "WebDialog"; + private static final String DISPLAY_TOUCH = "touch"; + private static final String USER_AGENT = "user_agent"; + static final String REDIRECT_URI = "fbconnect://success"; + static final String CANCEL_URI = "fbconnect://cancel"; + static final boolean DISABLE_SSL_CHECK_FOR_TESTING = false; + + // width below which there are no extra margins + private static final int NO_PADDING_SCREEN_WIDTH = 480; + // width beyond which we're always using the MIN_SCALE_FACTOR + private static final int MAX_PADDING_SCREEN_WIDTH = 800; + // height below which there are no extra margins + private static final int NO_PADDING_SCREEN_HEIGHT = 800; + // height beyond which we're always using the MIN_SCALE_FACTOR + private static final int MAX_PADDING_SCREEN_HEIGHT = 1280; + + // the minimum scaling factor for the web dialog (50% of screen size) + private static final double MIN_SCALE_FACTOR = 0.5; + // translucent border around the webview + private static final int BACKGROUND_GRAY = 0xCC000000; + + public static final int DEFAULT_THEME = android.R.style.Theme_Translucent_NoTitleBar; + + private String url; + private OnCompleteListener onCompleteListener; + private WebView webView; + private ProgressDialog spinner; + private ImageView crossImageView; + private FrameLayout contentFrameLayout; + private boolean listenerCalled = false; + private boolean isDetached = false; + + /** + * Interface that implements a listener to be called when the user's interaction with the + * dialog completes, whether because the dialog finished successfully, or it was cancelled, + * or an error was encountered. + */ + public interface OnCompleteListener { + /** + * Called when the dialog completes. + * + * @param values on success, contains the values returned by the dialog + * @param error on an error, contains an exception describing the error + */ + void onComplete(Bundle values, FacebookException error); + } + + /** + * Constructor which can be used to display a dialog with an already-constructed URL. + * + * @param context the context to use to display the dialog + * @param url the URL of the Web Dialog to display; no validation is done on this URL, but it should + * be a valid URL pointing to a Facebook Web Dialog + */ + public WebDialog(Context context, String url) { + this(context, url, DEFAULT_THEME); + } + + /** + * Constructor which can be used to display a dialog with an already-constructed URL and a custom theme. + * + * @param context the context to use to display the dialog + * @param url the URL of the Web Dialog to display; no validation is done on this URL, but it should + * be a valid URL pointing to a Facebook Web Dialog + * @param theme identifier of a theme to pass to the Dialog class + */ + public WebDialog(Context context, String url, int theme) { + super(context, theme); + this.url = url; + } + + /** + * Constructor which will construct the URL of the Web dialog based on the specified parameters. + * + * @param context the context to use to display the dialog + * @param action the portion of the dialog URL following "dialog/" + * @param parameters parameters which will be included as part of the URL + * @param theme identifier of a theme to pass to the Dialog class + * @param listener the listener to notify, or null if no notification is desired + */ + public WebDialog(Context context, String action, Bundle parameters, int theme, OnCompleteListener listener) { + super(context, theme); + + if (parameters == null) { + parameters = new Bundle(); + } + + // our webview client only handles the redirect uri we specify, so just hard code it here + parameters.putString(ServerProtocol.DIALOG_PARAM_REDIRECT_URI, REDIRECT_URI); + + parameters.putString(ServerProtocol.DIALOG_PARAM_DISPLAY, DISPLAY_TOUCH); + + Uri uri = Utility.buildUri( + ServerProtocol.getDialogAuthority(), + ServerProtocol.getAPIVersion() + "/" + ServerProtocol.DIALOG_PATH + action, + parameters); + this.url = uri.toString(); + onCompleteListener = listener; + } + + /** + * Sets the listener which will be notified when the dialog finishes. + * + * @param listener the listener to notify, or null if no notification is desired + */ + public void setOnCompleteListener(OnCompleteListener listener) { + onCompleteListener = listener; + } + + /** + * Gets the listener which will be notified when the dialog finishes. + * + * @return the listener, or null if none has been specified + */ + public OnCompleteListener getOnCompleteListener() { + return onCompleteListener; + } + + @Override + public void dismiss() { + if (webView != null) { + webView.stopLoading(); + } + if (!isDetached) { + if (spinner.isShowing()) { + spinner.dismiss(); + } + super.dismiss(); + } + } + + @Override + public void onDetachedFromWindow() { + isDetached = true; + super.onDetachedFromWindow(); + } + + @Override + public void onAttachedToWindow() { + isDetached = false; + super.onAttachedToWindow(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + sendCancelToListener(); + } + }); + + spinner = new ProgressDialog(getContext()); + spinner.requestWindowFeature(Window.FEATURE_NO_TITLE); + spinner.setMessage(getContext().getString(R.string.com_facebook_loading)); + spinner.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + sendCancelToListener(); + WebDialog.this.dismiss(); + } + }); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + contentFrameLayout = new FrameLayout(getContext()); + + // First calculate how big the frame layout should be + calculateSize(); + getWindow().setGravity(Gravity.CENTER); + + // resize the dialog if the soft keyboard comes up + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + + /* Create the 'x' image, but don't add to the contentFrameLayout layout yet + * at this point, we only need to know its drawable width and height + * to place the webview + */ + createCrossImage(); + + /* Now we know 'x' drawable width and height, + * layout the webview and add it the contentFrameLayout layout + */ + int crossWidth = crossImageView.getDrawable().getIntrinsicWidth(); + + setUpWebView(crossWidth / 2 + 1); + + /* Finally add the 'x' image to the contentFrameLayout layout and + * add contentFrameLayout to the Dialog view + */ + contentFrameLayout.addView(crossImageView, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + setContentView(contentFrameLayout); + } + + private void calculateSize() { + WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + display.getMetrics(metrics); + // always use the portrait dimensions to do the scaling calculations so we always get a portrait shaped + // web dialog + int width = metrics.widthPixels < metrics.heightPixels ? metrics.widthPixels : metrics.heightPixels; + int height = metrics.widthPixels < metrics.heightPixels ? metrics.heightPixels : metrics.widthPixels; + + int dialogWidth = Math.min( + getScaledSize(width, metrics.density, NO_PADDING_SCREEN_WIDTH, MAX_PADDING_SCREEN_WIDTH), + metrics.widthPixels); + int dialogHeight = Math.min( + getScaledSize(height, metrics.density, NO_PADDING_SCREEN_HEIGHT, MAX_PADDING_SCREEN_HEIGHT), + metrics.heightPixels); + + getWindow().setLayout(dialogWidth, dialogHeight); + } + + /** + * Returns a scaled size (either width or height) based on the parameters passed. + * @param screenSize a pixel dimension of the screen (either width or height) + * @param density density of the screen + * @param noPaddingSize the size at which there's no padding for the dialog + * @param maxPaddingSize the size at which to apply maximum padding for the dialog + * @return a scaled size. + */ + private int getScaledSize(int screenSize, float density, int noPaddingSize, int maxPaddingSize) { + int scaledSize = (int) ((float) screenSize / density); + double scaleFactor; + if (scaledSize <= noPaddingSize) { + scaleFactor = 1.0; + } else if (scaledSize >= maxPaddingSize) { + scaleFactor = MIN_SCALE_FACTOR; + } else { + // between the noPadding and maxPadding widths, we take a linear reduction to go from 100% + // of screen size down to MIN_SCALE_FACTOR + scaleFactor = MIN_SCALE_FACTOR + + ((double) (maxPaddingSize - scaledSize)) + / ((double) (maxPaddingSize - noPaddingSize)) + * (1.0 - MIN_SCALE_FACTOR); + } + return (int) (screenSize * scaleFactor); + } + + private void sendSuccessToListener(Bundle values) { + if (onCompleteListener != null && !listenerCalled) { + listenerCalled = true; + onCompleteListener.onComplete(values, null); + } + } + + private void sendErrorToListener(Throwable error) { + if (onCompleteListener != null && !listenerCalled) { + listenerCalled = true; + FacebookException facebookException = null; + if (error instanceof FacebookException) { + facebookException = (FacebookException) error; + } else { + facebookException = new FacebookException(error); + } + onCompleteListener.onComplete(null, facebookException); + } + } + + private void sendCancelToListener() { + sendErrorToListener(new FacebookOperationCanceledException()); + } + + private void createCrossImage() { + crossImageView = new ImageView(getContext()); + // Dismiss the dialog when user click on the 'x' + crossImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sendCancelToListener(); + WebDialog.this.dismiss(); + } + }); + Drawable crossDrawable = getContext().getResources().getDrawable(R.drawable.com_facebook_close); + crossImageView.setImageDrawable(crossDrawable); + /* 'x' should not be visible while webview is loading + * make it visible only after webview has fully loaded + */ + crossImageView.setVisibility(View.INVISIBLE); + } + + @SuppressLint("SetJavaScriptEnabled") + private void setUpWebView(int margin) { + LinearLayout webViewContainer = new LinearLayout(getContext()); + webView = new WebView(getContext()); + webView.setVerticalScrollBarEnabled(false); + webView.setHorizontalScrollBarEnabled(false); + webView.setWebViewClient(new DialogWebViewClient()); + webView.getSettings().setJavaScriptEnabled(true); + webView.loadUrl(url); + webView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + webView.setVisibility(View.INVISIBLE); + webView.getSettings().setSavePassword(false); + webView.getSettings().setSaveFormData(false); + + webViewContainer.setPadding(margin, margin, margin, margin); + webViewContainer.addView(webView); + webViewContainer.setBackgroundColor(BACKGROUND_GRAY); + contentFrameLayout.addView(webViewContainer); + } + + private class DialogWebViewClient extends WebViewClient { + @Override + @SuppressWarnings("deprecation") + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Utility.logd(LOG_TAG, "Redirect URL: " + url); + if (url.startsWith(WebDialog.REDIRECT_URI)) { + Bundle values = Util.parseUrl(url); + + String error = values.getString("error"); + if (error == null) { + error = values.getString("error_type"); + } + + String errorMessage = values.getString("error_msg"); + if (errorMessage == null) { + errorMessage = values.getString("error_description"); + } + String errorCodeString = values.getString("error_code"); + int errorCode = FacebookRequestError.INVALID_ERROR_CODE; + if (!Utility.isNullOrEmpty(errorCodeString)) { + try { + errorCode = Integer.parseInt(errorCodeString); + } catch (NumberFormatException ex) { + errorCode = FacebookRequestError.INVALID_ERROR_CODE; + } + } + + if (Utility.isNullOrEmpty(error) && Utility + .isNullOrEmpty(errorMessage) && errorCode == FacebookRequestError.INVALID_ERROR_CODE) { + sendSuccessToListener(values); + } else if (error != null && (error.equals("access_denied") || + error.equals("OAuthAccessDeniedException"))) { + sendCancelToListener(); + } else { + FacebookRequestError requestError = new FacebookRequestError(errorCode, error, errorMessage); + sendErrorToListener(new FacebookServiceException(requestError, errorMessage)); + } + + WebDialog.this.dismiss(); + return true; + } else if (url.startsWith(WebDialog.CANCEL_URI)) { + sendCancelToListener(); + WebDialog.this.dismiss(); + return true; + } else if (url.contains(DISPLAY_TOUCH)) { + return false; + } + // launch non-dialog URLs in a full browser + getContext().startActivity( + new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + return true; + } + + @Override + public void onReceivedError(WebView view, int errorCode, + String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + sendErrorToListener(new FacebookDialogException(description, errorCode, failingUrl)); + WebDialog.this.dismiss(); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + if (DISABLE_SSL_CHECK_FOR_TESTING) { + handler.proceed(); + } else { + super.onReceivedSslError(view, handler, error); + + sendErrorToListener(new FacebookDialogException(null, ERROR_FAILED_SSL_HANDSHAKE, null)); + handler.cancel(); + WebDialog.this.dismiss(); + } + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + Utility.logd(LOG_TAG, "Webview loading URL: " + url); + super.onPageStarted(view, url, favicon); + if (!isDetached) { + spinner.show(); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (!isDetached) { + spinner.dismiss(); + } + /* + * Once web view is fully loaded, set the contentFrameLayout background to be transparent + * and make visible the 'x' image. + */ + contentFrameLayout.setBackgroundColor(Color.TRANSPARENT); + webView.setVisibility(View.VISIBLE); + crossImageView.setVisibility(View.VISIBLE); + } + } + + private static class BuilderBase> { + private Context context; + private Session session; + private String applicationId; + private String action; + private int theme = DEFAULT_THEME; + private OnCompleteListener listener; + private Bundle parameters; + + protected BuilderBase(Context context, String action) { + Session activeSession = Session.getActiveSession(); + if (activeSession != null && activeSession.isOpened()) { + this.session = activeSession; + } else { + String applicationId = Utility.getMetadataApplicationId(context); + if (applicationId != null) { + this.applicationId = applicationId; + } else { + throw new FacebookException("Attempted to create a builder without an open" + + " Active Session or a valid default Application ID."); + } + } + finishInit(context, action, null); + } + + protected BuilderBase(Context context, Session session, String action, Bundle parameters) { + Validate.notNull(session, "session"); + if (!session.isOpened()) { + throw new FacebookException("Attempted to use a Session that was not open."); + } + this.session = session; + + finishInit(context, action, parameters); + } + + protected BuilderBase(Context context, String applicationId, String action, Bundle parameters) { + if (applicationId == null) { + applicationId = Utility.getMetadataApplicationId(context); + } + Validate.notNullOrEmpty(applicationId, "applicationId"); + this.applicationId = applicationId; + + finishInit(context, action, parameters); + } + + /** + * Sets a theme identifier which will be passed to the underlying Dialog. + * + * @param theme a theme identifier which will be passed to the Dialog class + * @return the builder + */ + public CONCRETE setTheme(int theme) { + this.theme = theme; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Sets the listener which will be notified when the dialog finishes. + * + * @param listener the listener to notify, or null if no notification is desired + * @return the builder + */ + public CONCRETE setOnCompleteListener(OnCompleteListener listener) { + this.listener = listener; + @SuppressWarnings("unchecked") + CONCRETE result = (CONCRETE) this; + return result; + } + + /** + * Constructs a WebDialog using the parameters provided. The dialog is not shown, + * but is ready to be shown by calling Dialog.show(). + * + * @return the WebDialog + */ + public WebDialog build() { + if (session != null && session.isOpened()) { + parameters.putString(ServerProtocol.DIALOG_PARAM_APP_ID, session.getApplicationId()); + parameters.putString(ServerProtocol.DIALOG_PARAM_ACCESS_TOKEN, session.getAccessToken()); + } else { + parameters.putString(ServerProtocol.DIALOG_PARAM_APP_ID, applicationId); + } + + return new WebDialog(context, action, parameters, theme, listener); + } + + protected String getApplicationId() { + return applicationId; + } + + protected Context getContext() { + return context; + } + + protected int getTheme() { + return theme; + } + + protected Bundle getParameters() { + return parameters; + } + + protected WebDialog.OnCompleteListener getListener() { + return listener; + } + + private void finishInit(Context context, String action, Bundle parameters) { + this.context = context; + this.action = action; + if (parameters != null) { + this.parameters = parameters; + } else { + this.parameters = new Bundle(); + } + } + } + + /** + * Provides a builder that allows construction of an arbitary Facebook web dialog. + */ + public static class Builder extends BuilderBase { + /** + * Constructor that builds a dialog using either the active session, or the application + * id specified in the application/meta-data. + * + * @param context the Context within which the dialog will be shown. + * @param action the portion of the dialog URL following www.facebook.com/dialog/. + * See https://developers.facebook.com/docs/reference/dialogs/ for details. + */ + public Builder(Context context, String action) { + super(context, action); + } + + /** + * Constructor that builds a dialog for an authenticated user. + * + * @param context the Context within which the dialog will be shown. + * @param session the Session representing an authenticating user to use for + * showing the dialog; must not be null, and must be opened. + * @param action the portion of the dialog URL following www.facebook.com/dialog/. + * See https://developers.facebook.com/docs/reference/dialogs/ for details. + * @param parameters a Bundle containing parameters to pass as part of the URL. + */ + public Builder(Context context, Session session, String action, Bundle parameters) { + super(context, session, action, parameters); + } + + /** + * Constructor that builds a dialog without an authenticated user. + * + * @param context the Context within which the dialog will be shown. + * @param applicationId the application ID to be included in the dialog URL. + * @param action the portion of the dialog URL following www.facebook.com/dialog/. + * See https://developers.facebook.com/docs/reference/dialogs/ for details. + * @param parameters a Bundle containing parameters to pass as part of the URL. + */ + public Builder(Context context, String applicationId, String action, Bundle parameters) { + super(context, applicationId, action, parameters); + } + } + + /** + * Provides a builder that allows construction of the parameters for showing + * the Feed Dialog. + */ + public static class FeedDialogBuilder extends BuilderBase { + private static final String FEED_DIALOG = "feed"; + private static final String FROM_PARAM = "from"; + private static final String TO_PARAM = "to"; + private static final String LINK_PARAM = "link"; + private static final String PICTURE_PARAM = "picture"; + private static final String SOURCE_PARAM = "source"; + private static final String NAME_PARAM = "name"; + private static final String CAPTION_PARAM = "caption"; + private static final String DESCRIPTION_PARAM = "description"; + + /** + * Constructor that builds a Feed Dialog using either the active session, or the application + * ID specified in the application/meta-data. + * + * @param context the Context within which the dialog will be shown. + */ + public FeedDialogBuilder(Context context) { + super(context, FEED_DIALOG); + } + + /** + * Constructor that builds a Feed Dialog using the provided session. + * + * @param context the Context within which the dialog will be shown. + * @param session the Session representing an authenticating user to use for + * showing the dialog; must not be null, and must be opened. + */ + public FeedDialogBuilder(Context context, Session session) { + super(context, session, FEED_DIALOG, null); + } + + /** + * Constructor that builds a Feed Dialog using the provided session and parameters. + * + * @param context the Context within which the dialog will be shown. + * @param session the Session representing an authenticating user to use for + * showing the dialog; must not be null, and must be opened. + * @param parameters a Bundle containing parameters to pass as part of the + * dialog URL. No validation is done on these parameters; it is + * the caller's responsibility to ensure they are valid. For more information, + * see + * https://developers.facebook.com/docs/reference/dialogs/feed/. + */ + public FeedDialogBuilder(Context context, Session session, Bundle parameters) { + super(context, session, FEED_DIALOG, parameters); + } + + /** + * Constructor that builds a Feed Dialog using the provided application ID and parameters. + * + * @param context the Context within which the dialog will be shown. + * @param applicationId the application ID to use. If null, the application ID specified in the + * application/meta-data will be used instead. + * @param parameters a Bundle containing parameters to pass as part of the + * dialog URL. No validation is done on these parameters; it is + * the caller's responsibility to ensure they are valid. For more information, + * see + * https://developers.facebook.com/docs/reference/dialogs/feed/. + */ + public FeedDialogBuilder(Context context, String applicationId, Bundle parameters) { + super(context, applicationId, FEED_DIALOG, parameters); + } + + /** + * Sets the ID of the profile that is posting to Facebook. If none is specified, + * the default is "me". This profile must be either the authenticated user or a + * Page that the user is an administrator of. + * + * @param id Facebook ID of the profile to post from + * @return the builder + */ + public FeedDialogBuilder setFrom(String id) { + getParameters().putString(FROM_PARAM, id); + return this; + } + + /** + * Sets the ID of the profile that the story will be published to. If not specified, it + * will default to the same profile that the story is being published from. The ID must be a friend who also + * uses your app. + * + * @param id Facebook ID of the profile to post to + * @return the builder + */ + public FeedDialogBuilder setTo(String id) { + getParameters().putString(TO_PARAM, id); + return this; + } + + /** + * Sets the URL of a link to be shared. + * + * @param link the URL + * @return the builder + */ + public FeedDialogBuilder setLink(String link) { + getParameters().putString(LINK_PARAM, link); + return this; + } + + /** + * Sets the URL of a picture to be shared. + * + * @param picture the URL of the picture + * @return the builder + */ + public FeedDialogBuilder setPicture(String picture) { + getParameters().putString(PICTURE_PARAM, picture); + return this; + } + + /** + * Sets the URL of a media file attached to this post. If this is set, any picture + * set via setPicture will be ignored. + * + * @param source the URL of the media file + * @return the builder + */ + public FeedDialogBuilder setSource(String source) { + getParameters().putString(SOURCE_PARAM, source); + return this; + } + + /** + * Sets the name of the item being shared. + * + * @param name the name + * @return the builder + */ + public FeedDialogBuilder setName(String name) { + getParameters().putString(NAME_PARAM, name); + return this; + } + + /** + * Sets the caption to be displayed. + * + * @param caption the caption + * @return the builder + */ + public FeedDialogBuilder setCaption(String caption) { + getParameters().putString(CAPTION_PARAM, caption); + return this; + } + + /** + * Sets the description to be displayed. + * + * @param description the description + * @return the builder + */ + public FeedDialogBuilder setDescription(String description) { + getParameters().putString(DESCRIPTION_PARAM, description); + return this; + } + } + + /** + * Provides a builder that allows construction of the parameters for showing + * the Requests Dialog. + */ + public static class RequestsDialogBuilder extends BuilderBase { + private static final String APPREQUESTS_DIALOG = "apprequests"; + private static final String MESSAGE_PARAM = "message"; + private static final String TO_PARAM = "to"; + private static final String DATA_PARAM = "data"; + private static final String TITLE_PARAM = "title"; + + /** + * Constructor that builds a Requests Dialog using either the active session, or the application + * ID specified in the application/meta-data. + * + * @param context the Context within which the dialog will be shown. + */ + public RequestsDialogBuilder(Context context) { + super(context, APPREQUESTS_DIALOG); + } + + /** + * Constructor that builds a Requests Dialog using the provided session. + * + * @param context the Context within which the dialog will be shown. + * @param session the Session representing an authenticating user to use for + * showing the dialog; must not be null, and must be opened. + */ + public RequestsDialogBuilder(Context context, Session session) { + super(context, session, APPREQUESTS_DIALOG, null); + } + + /** + * Constructor that builds a Requests Dialog using the provided session and parameters. + * + * @param context the Context within which the dialog will be shown. + * @param session the Session representing an authenticating user to use for + * showing the dialog; must not be null, and must be opened. + * @param parameters a Bundle containing parameters to pass as part of the + * dialog URL. No validation is done on these parameters; it is + * the caller's responsibility to ensure they are valid. For more information, + * see + * https://developers.facebook.com/docs/reference/dialogs/requests/. + */ + public RequestsDialogBuilder(Context context, Session session, Bundle parameters) { + super(context, session, APPREQUESTS_DIALOG, parameters); + } + + /** + * Constructor that builds a Requests Dialog using the provided application ID and parameters. + * + * @param context the Context within which the dialog will be shown. + * @param applicationId the application ID to use. If null, the application ID specified in the + * application/meta-data will be used instead. + * @param parameters a Bundle containing parameters to pass as part of the + * dialog URL. No validation is done on these parameters; it is + * the caller's responsibility to ensure they are valid. For more information, + * see + * https://developers.facebook.com/docs/reference/dialogs/requests/. + */ + public RequestsDialogBuilder(Context context, String applicationId, Bundle parameters) { + super(context, applicationId, APPREQUESTS_DIALOG, parameters); + } + + /** + * Sets the string users receiving the request will see. The maximum length + * is 60 characters. + * + * @param message the message + * @return the builder + */ + public RequestsDialogBuilder setMessage(String message) { + getParameters().putString(MESSAGE_PARAM, message); + return this; + } + + /** + * Sets the user ID or user name the request will be sent to. If this is not + * specified, a friend selector will be displayed and the user can select up + * to 50 friends. + * + * @param id the id or user name to send the request to + * @return the builder + */ + public RequestsDialogBuilder setTo(String id) { + getParameters().putString(TO_PARAM, id); + return this; + } + + /** + * Sets optional data which can be used for tracking; maximum length is 255 + * characters. + * + * @param data the data + * @return the builder + */ + public RequestsDialogBuilder setData(String data) { + getParameters().putString(DATA_PARAM, data); + return this; + } + + /** + * Sets an optional title for the dialog; maximum length is 50 characters. + * + * @param title the title + * @return the builder + */ + public RequestsDialogBuilder setTitle(String title) { + getParameters().putString(TITLE_PARAM, title); + return this; + } + } +} diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_focused.9.png new file mode 100644 index 0000000000..cf60eb0acf Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_normal.9.png new file mode 100644 index 0000000000..ece0c4c940 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_pressed.9.png new file mode 100644 index 0000000000..a123c25554 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_blue_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_focused.9.png new file mode 100644 index 0000000000..2e6f66dbc5 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_normal.9.png new file mode 100644 index 0000000000..6098f0b0e6 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_pressed.9.png new file mode 100644 index 0000000000..23b9757d15 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_button_grey_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_close.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_close.png new file mode 100644 index 0000000000..d925cb7b20 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_close.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_inverse_icon.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_inverse_icon.png new file mode 100644 index 0000000000..a7289c1125 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_inverse_icon.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_logo.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_logo.png new file mode 100644 index 0000000000..26ab7945fc Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_logo.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_picker_magnifier.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_picker_magnifier.png new file mode 100644 index 0000000000..d6ec37d04b Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_picker_magnifier.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_background.9.png new file mode 100644 index 0000000000..4707ba20c8 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_bottomnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_bottomnub.png new file mode 100644 index 0000000000..83bf9694b6 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_bottomnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_topnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_topnub.png new file mode 100644 index 0000000000..0d8fb82b61 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_topnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_xout.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_xout.png new file mode 100644 index 0000000000..5bebe5e357 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_black_xout.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_background.9.png new file mode 100644 index 0000000000..0f8c5dffcf Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_bottomnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_bottomnub.png new file mode 100644 index 0000000000..21145bca67 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_bottomnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_topnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_topnub.png new file mode 100644 index 0000000000..8b534773d1 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_topnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_xout.png b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_xout.png new file mode 100644 index 0000000000..9f341075c0 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-hdpi/com_facebook_tooltip_blue_xout.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-ldpi/com_facebook_close.png b/android/KMAPro/facebookSDK/src/main/res/drawable-ldpi/com_facebook_close.png new file mode 100644 index 0000000000..fe4be250e5 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-ldpi/com_facebook_close.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_focused.9.png new file mode 100644 index 0000000000..cfb7a015c7 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_normal.9.png new file mode 100644 index 0000000000..1e93901338 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_pressed.9.png new file mode 100644 index 0000000000..d8427fdefd Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_button_blue_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_inverse_icon.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_inverse_icon.png new file mode 100644 index 0000000000..a806a2f5dc Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_inverse_icon.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_picker_magnifier.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_picker_magnifier.png new file mode 100644 index 0000000000..c5170c18bd Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_picker_magnifier.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_background.9.png new file mode 100644 index 0000000000..59f2639828 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_bottomnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_bottomnub.png new file mode 100644 index 0000000000..4c97bf9238 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_bottomnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_topnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_topnub.png new file mode 100644 index 0000000000..7fdf08201d Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_topnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_xout.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_xout.png new file mode 100644 index 0000000000..21ad6a54e7 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_black_xout.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_background.9.png new file mode 100644 index 0000000000..bd54278dae Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_bottomnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_bottomnub.png new file mode 100644 index 0000000000..299afc24ed Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_bottomnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_topnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_topnub.png new file mode 100644 index 0000000000..cb29b55f30 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_topnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_xout.png b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_xout.png new file mode 100644 index 0000000000..73695f03b8 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-mdpi/com_facebook_tooltip_blue_xout.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_focused.9.png new file mode 100644 index 0000000000..5a47068d7c Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_normal.9.png new file mode 100644 index 0000000000..1449c8a13f Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_pressed.9.png new file mode 100644 index 0000000000..28bd184ed3 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_blue_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_focused.9.png new file mode 100644 index 0000000000..bfd883f920 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_normal.9.png new file mode 100644 index 0000000000..aa98954324 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_pressed.9.png new file mode 100644 index 0000000000..92f2ad12ac Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_button_grey_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_close.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_close.png new file mode 100644 index 0000000000..e3aff5ae5f Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_close.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_inverse_icon.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_inverse_icon.png new file mode 100644 index 0000000000..4192c48870 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_inverse_icon.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_logo.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_logo.png new file mode 100644 index 0000000000..24d7fc56b5 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_logo.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_picker_magnifier.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_picker_magnifier.png new file mode 100644 index 0000000000..a5b372468c Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_picker_magnifier.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_background.9.png new file mode 100644 index 0000000000..d6f89a731b Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_bottomnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_bottomnub.png new file mode 100644 index 0000000000..71abe544b9 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_bottomnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_topnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_topnub.png new file mode 100644 index 0000000000..2ac32dbc24 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_topnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_xout.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_xout.png new file mode 100644 index 0000000000..e3d562b609 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_black_xout.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_background.9.png new file mode 100644 index 0000000000..a1978044ca Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_bottomnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_bottomnub.png new file mode 100644 index 0000000000..cee80f1298 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_bottomnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_topnub.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_topnub.png new file mode 100644 index 0000000000..2ec445e5dd Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_topnub.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_xout.png b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_xout.png new file mode 100644 index 0000000000..7fd57632d6 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable-xhdpi/com_facebook_tooltip_blue_xout.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue.xml new file mode 100644 index 0000000000..91aebe6856 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_focused.9.png new file mode 100644 index 0000000000..cfb7a015c7 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_normal.9.png new file mode 100644 index 0000000000..1e93901338 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_pressed.9.png new file mode 100644 index 0000000000..d8427fdefd Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_blue_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check.xml new file mode 100644 index 0000000000..dfa5106073 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check_off.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check_off.png new file mode 100644 index 0000000000..e9737df5c5 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check_off.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check_on.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check_on.png new file mode 100644 index 0000000000..d793151e64 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_check_on.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_focused.9.png new file mode 100644 index 0000000000..56f6035719 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_normal.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_normal.9.png new file mode 100644 index 0000000000..dff3b7d73e Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_normal.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_pressed.9.png new file mode 100644 index 0000000000..46648efcdd Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_button_grey_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_close.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_close.png new file mode 100644 index 0000000000..ad0147460f Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_close.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_inverse_icon.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_inverse_icon.png new file mode 100644 index 0000000000..a806a2f5dc Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_inverse_icon.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_list_divider.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_list_divider.9.png new file mode 100644 index 0000000000..7d4c46c477 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_list_divider.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_list_section_header_background.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_list_section_header_background.9.png new file mode 100644 index 0000000000..1763c4a648 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_list_section_header_background.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_loginbutton_silver.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_loginbutton_silver.xml new file mode 100644 index 0000000000..adf9885d7f --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_loginbutton_silver.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_logo.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_logo.png new file mode 100644 index 0000000000..785acf9eac Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_logo.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_item_background.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_item_background.xml new file mode 100644 index 0000000000..e43eb6cf5d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_item_background.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_focused.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_focused.9.png new file mode 100644 index 0000000000..7c0599e3a6 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_focused.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_longpressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_longpressed.9.png new file mode 100644 index 0000000000..3bf8e03623 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_longpressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_pressed.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_pressed.9.png new file mode 100644 index 0000000000..6e77525d2d Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_pressed.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector.xml new file mode 100644 index 0000000000..b35deba5ef --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector_background_transition.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector_background_transition.xml new file mode 100644 index 0000000000..0e4aa8eb8c --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector_background_transition.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector_disabled.9.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector_disabled.9.png new file mode 100644 index 0000000000..42cb6463e4 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_list_selector_disabled.9.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_top_button.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_top_button.xml new file mode 100644 index 0000000000..d282105b00 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_picker_top_button.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_place_default_icon.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_place_default_icon.png new file mode 100644 index 0000000000..97ec3e608c Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_place_default_icon.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_default_icon.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_default_icon.png new file mode 100644 index 0000000000..15864ccdb9 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_default_icon.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_picture_blank_portrait.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_picture_blank_portrait.png new file mode 100644 index 0000000000..107d7f89e6 Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_picture_blank_portrait.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_picture_blank_square.png b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_picture_blank_square.png new file mode 100644 index 0000000000..8b4a76a0ef Binary files /dev/null and b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_profile_picture_blank_square.png differ diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_top_background.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_top_background.xml new file mode 100644 index 0000000000..9b29897096 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_top_background.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_top_button.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_top_button.xml new file mode 100644 index 0000000000..7a60e48442 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_top_button.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_usersettingsfragment_background_gradient.xml b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_usersettingsfragment_background_gradient.xml new file mode 100644 index 0000000000..b28dab56c5 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/drawable/com_facebook_usersettingsfragment_background_gradient.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_friendpickerfragment.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_friendpickerfragment.xml new file mode 100644 index 0000000000..b42f15e7d7 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_friendpickerfragment.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_login_activity_layout.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_login_activity_layout.xml new file mode 100644 index 0000000000..09b28999fd --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_login_activity_layout.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_activity_circle_row.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_activity_circle_row.xml new file mode 100644 index 0000000000..de3e7e3235 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_activity_circle_row.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_checkbox.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_checkbox.xml new file mode 100644 index 0000000000..ecc4aa88e4 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_checkbox.xml @@ -0,0 +1,25 @@ + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_image.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_image.xml new file mode 100644 index 0000000000..dd02cfe9c0 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_image.xml @@ -0,0 +1,24 @@ + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_list_row.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_list_row.xml new file mode 100644 index 0000000000..8f6725b11d --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_list_row.xml @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_list_section_header.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_list_section_header.xml new file mode 100644 index 0000000000..ef04e3bc97 --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_list_section_header.xml @@ -0,0 +1,34 @@ + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_search_box.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_search_box.xml new file mode 100644 index 0000000000..616bba4e0b --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_search_box.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_title_bar.xml b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_title_bar.xml new file mode 100644 index 0000000000..b50cfb1aca --- /dev/null +++ b/android/KMAPro/facebookSDK/src/main/res/layout/com_facebook_picker_title_bar.xml @@ -0,0 +1,64 @@ + + + + +