diff --git a/README.md b/README.md index 2eef5366c..5e9cae0e3 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ The Facebook plugin for [Apache Cordova](http://incubator.apache.org/cordova/) a * Supported on PhoneGap (Cordova) v3.3.0 and above. * This plugin is built for - * iOS FacebookSDK 3.16.1 - * Android FacebookSDK 3.16.0 + * iOS FacebookSDK 3.20.0 + * Android FacebookSDK 3.20.0 * GitHub URL : [https://github.com/Wizcorp/phonegap-facebook-plugin/](https://github.com/Wizcorp/phonegap-facebook-plugin/) ## << --- Cordova Registry Warning [iOS] @@ -30,7 +30,10 @@ To use this plugin you will need to make sure you've registered your Facebook ap - [Web App Guide](platforms/web/README.md) -- [PhoneGap Build](platforms/pg-build/README.md) +- [PhoneGap Build Guide](platforms/pg-build/README.md) + +- [Troubleshooting Guide | F.A.Q.](TROUBLESHOOTING.md) + #### Example Apps diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 000000000..e2f09c5cf --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,145 @@ +# Troubleshooting + +If you cannot solve your issue here please check the **existing issues** on Github, if you still cannot find a solution please [create an issue](https://github.com/Wizcorp/phonegap-facebook-plugin/issues). + +When creating a Github issue **remember to**: + +- List your platform! +- Provide sample code +- Provide a log (Xcode console or adb logcat) + +### Troubleshooting contents +- [**General**](#general) + - [How do I Build without Eclipse?](#how-do-i-build-without-eclipse) + - [How do I Add a Like Button?](#how-do-i-add-a-like-button) + - [Where is the init API?](#where-is-the-init-api) + - [How to install with NPM PhoneGap?](#how-to-install-with-npm-phonegap) + +- [**Android**](#android) + - [No Reply From Login?](#no-reply-from-login) + - [My Hash Does Not Work, I am Using Windows](#my-hash-does-not-work-i-am-using-windows) + - [Jar mismatch! Fix your dependencies](#jar-mismatch-fix-your-dependencies) + +- [**iOS**](#ios) + - [Missing FacebookConnectPlugin](#missing-facebookconnectplugin) + + +## General +### How do I Build without Eclipse? + +- Solution + - Check the [Android Guide](https://github.com/Wizcorp/phonegap-facebook-plugin/blob/master/platforms/android/README.md) + +### How do I Add a Like Button? + +- Problem + - I am trying to implement like button functionality in my app. Whenever user likes my facebook page, user will get rewards. So i have tried to implement this like button functionality as described here: +[https://developers.facebook.com/docs/plugins/like-button](https://developers.facebook.com/docs/plugins/like-button) + +- Solution + - It won't work for native apps because: [https://developers.facebook.com/docs/ios/like-button/](https://developers.facebook.com/docs/ios/like-button/) is not supported yet. **HOWEVER**; it can be done with the graph API [http://stackoverflow.com/questions/11915269/built-in-like-button-facebook-sdk-3-0](http://stackoverflow.com/questions/11915269/built-in-like-button-facebook-sdk-3-0) + Things you have to take care of are : + + 1. Your like button must not be the same (graphically) as the Facebook like button + + 2. When you display your page / button you have to call the getLoginStatus method first to know if the current user is connected to its Facebook account. If he is connected then call `GET` [https://graph.facebook.com/me/og.likes?access_token=FB_ACCESS_TOKEN&object=URL_TO_LIKE](https://graph.facebook.com/me/og.likes?access_token=FB_ACCESS_TOKEN&object=URL_TO_LIKE) with the Facebook Access Token returned by the g3. etAccessToken method (if this returns data then style your like button with a red heart for example, a grey heart if the call returns an empty array). + + 3. To create a like (when your user clicks on your like button and your like button is a grey heart) do a POST on [https://graph.facebook.com/me/og.likes?access_token=FB_ACCESS_TOKEN&object=URL_TO_LIKE](https://graph.facebook.com/me/og.likes?access_token=FB_ACCESS_TOKEN&object=URL_TO_LIKE) + + 4. To remove a like (when your user clicks on your like button and your like button is a red heart) do a `DELETE` on [https://graph.facebook.com/LIKE_IDENTIFIER?access_token=FB_ACCESS_TOKEN](https://graph.facebook.com/LIKE_IDENTIFIER?access_token=FB_ACCESS_TOKEN). The `LIKE_IDENTIFIER` is returned from steps 2 or 3. + +The better way to understand this little "workflow" is to manipulate the Graph API on the og.likes endpoint using the [Facebook Graph Explorer](https://developers.facebook.com/tools/explorer) tool. + +### Where is the init API? + +- Problem + - I was using `FB.init()` and now it's not working. + +- Solution + - You are using an out-dated API. Please check the [new API with sample code](https://github.com/Wizcorp/phonegap-facebook-plugin/blob/master/README.md) and sample projects in `platforms/ios` and `platforms/android`. + +### How to install with NPM PhoneGap? + +- Problem + - I'm trying to install via https the Facebook plugin in iOS, but when I try the following line: + +`sudo phonegap local plugin add https://github.com/phonegap/phonegap-facebook-plugin.git --variable APP_ID="12345678910" --variable APP_NAME="MyAPP"` + +Im getting the message "[error] Variable(s) missing: APP_ID, APP_NAME" + +- Solution + - The `PhoneGap` CLI and `Cordova` CLI differ slightly you will need to run: + +`git clone https://github.com/Wizcorp/phonegap-facebook-plugin` + +`cd to/your/project` + +`phonegap local plugin add /path/to/here/phonegap-facebook-plugin --variable APP_ID="12345678910" --variable APP_NAME="AwesomeApp"` + +## Android +### No Reply From Login? + +- Problem + - **facebookConnectPlugin.login doesn't call neither success nor faillure methods.** - When I'm disconnected from Facebook and don't have the native app, the iframe fallback is blank. Checking on chrome inspector, the elements are set to display: none. + +- Solution + - Copy and paste the following code to print your hash. Add the hash to your Facebook Developer Dashboard. + +``` +try { + PackageInfo info = + cordova.getActivity().getPackageManager().getPackageInfo("com.goapes.golearn", PackageManager.GET_SIGNATURES); + + for (Signature signature : info.signatures) { + MessageDigest md = MessageDigest.getInstance("SHA"); + md.update(signature.toByteArray()); + Log.e("MY KEY HASH:", Base64.encodeToString(md.digest(), Base64.DEFAULT)); + } + +} catch (NameNotFoundException e) { + +} catch (NoSuchAlgorithmException e) { + +} +``` + +### My Hash Does Not Work, I am Using Windows + +- Problem + - Windows users have to be careful about openssl-for-windows [http://code.google.com/p/openssl-for-windows/downloads/list](http://code.google.com/p/openssl-for-windows/downloads/list), the latest version, at least on plataform 64bit, does not generate the correct hash that Facebook needs for android apps. + +- Solution + - Use one of these versions when creating your hash: **openssl-0.9.8e_X64.zip** or **openssl-0.9.8d_X64.rar** + +You should **not** use the openssl-0.9.8k_X64.zip. + +### Jar mismatch! Fix your dependencies + +- Problem + - I get this error: + +``` +BUILD FAILED +/usr/local/opt/android-sdk/tools/ant/build.xml:577: Jar mismatch! Fix your dependencies +``` + +- Solution + - You may have duplicate android-support-v4.jar files. Remove android-support-v4.jar from the `/libs` folder of your project. + +## iOS +### Missing FacebookConnectPlugin +- Problem: + - `CDVPlugin class FacebookConnectPlugin (pluginName: facebookconnectplugin) does not exist.` +- Solution: + 1. Open up Xcode + 2. Go to "Build Phases" + 3. Ensure that the following file is added under "Compile Sources": + - `FacebookConnectPlugin.m` + 4. Ensure that the following is added under "Link Binary With Libraris": + - `FacebookSDK.framework` + - `libsqlite3.dylib` + - `Social.framework` + - `Accounts.framework` + - `Security.framework` + +Cordova and plugman seems to have some problems adding frameworks etc. when re-installing/upgrading plugins. diff --git a/platforms/android/FacebookLib/build.gradle b/platforms/android/FacebookLib/build.gradle index d043dcea0..f0f7fdb48 100644 --- a/platforms/android/FacebookLib/build.gradle +++ b/platforms/android/FacebookLib/build.gradle @@ -1,4 +1,10 @@ -apply plugin: 'android-library' +apply plugin: 'com.android.library' + +repositories { + mavenCentral() +} + +project.group = 'com.facebook.android' dependencies { compile 'com.android.support:support-v4:20.0.+' @@ -26,3 +32,100 @@ android { } } } + +apply plugin: 'maven' +apply plugin: 'signing' + +def isSnapshot = version.endsWith('-SNAPSHOT') +def ossrhUsername = hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" +def ossrhPassword = hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" + +task setVersion { + // The version will be derived from source + project.version = null + def sdkVersionFile = file('src/com/facebook/FacebookSdkVersion.java') + sdkVersionFile.eachLine{ + def matcher = (it =~ /(?:.*BUILD = \")(.*)(?:\".*)/) + if (matcher.matches()) { + project.version = matcher[0][1] + return + } + } + if (project.version.is('unspecified')) { + throw new GradleScriptException('Version could not be found.', null) + } +} + +uploadArchives { + repositories.mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + pom.project { + name 'Facebook-Android-SDK' + artifactId = 'facebook-android-sdk' + packaging 'aar' + description 'Facebook Android SDK' + url 'https://github.com/facebook/facebook-android-sdk' + + scm { + connection 'scm:git@github.com:facebook/facebook-android-sdk.git' + developerConnection 'scm:git@github.com:facebook/facebook-android-sdk.git' + url 'https://github.com/facebook/facebook-android-sdk' + } + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'https://github.com/facebook/facebook-android-sdk/blob/master/LICENSE.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'facebook' + name 'Facebook' + } + } + } + } +} + +uploadArchives.dependsOn(setVersion) + +signing { + required { !isSnapshot && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives +} + +task androidJavadocs(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { + classifier = 'javadoc' + from androidJavadocs.destinationDir +} + +task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles +} + +artifacts { + archives androidSourcesJar + archives androidJavadocsJar +} + +afterEvaluate { + androidJavadocs.classpath += project.android.libraryVariants.toList().first().javaCompile.classpath +} diff --git a/platforms/android/FacebookLib/custom_rules.xml b/platforms/android/FacebookLib/custom_rules.xml new file mode 100644 index 000000000..7ccb46b18 --- /dev/null +++ b/platforms/android/FacebookLib/custom_rules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/platforms/android/FacebookLib/libs/android-support-v4.jar b/platforms/android/FacebookLib/libs/android-support-v4.jar index c31cede47..ac4181fca 100644 Binary files a/platforms/android/FacebookLib/libs/android-support-v4.jar and b/platforms/android/FacebookLib/libs/android-support-v4.jar differ diff --git a/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_background.png b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_background.png new file mode 100644 index 000000000..32814c82a Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_background.png differ diff --git a/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_background_selected.png b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_background_selected.png new file mode 100644 index 000000000..b2c580e2a Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_background_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_icon.png b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_icon.png new file mode 100644 index 000000000..1dc9f6d24 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_icon.png differ diff --git a/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_icon_selected.png b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_icon_selected.png new file mode 100644 index 000000000..722362ae8 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_icon_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_pressed.png b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_pressed.png new file mode 100644 index 000000000..047eaa6c4 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-hdpi/com_facebook_button_like_pressed.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_background.png b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_background.png new file mode 100644 index 000000000..3c25c433e Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_background.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_background_selected.png b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_background_selected.png new file mode 100644 index 000000000..accfdfc6e Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_background_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_icon.png b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_icon.png new file mode 100644 index 000000000..db8fcad20 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_icon.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_icon_selected.png b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_icon_selected.png new file mode 100644 index 000000000..96e9a308b Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_icon_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_pressed.png b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_pressed.png new file mode 100644 index 000000000..d990a4e53 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xhdpi/com_facebook_button_like_pressed.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_background.png b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_background.png new file mode 100644 index 000000000..bead08dc5 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_background.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_background_selected.png b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_background_selected.png new file mode 100644 index 000000000..85e4e873c Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_background_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_icon.png b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_icon.png new file mode 100644 index 000000000..1d67cab7b Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_icon.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_icon_selected.png b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_icon_selected.png new file mode 100644 index 000000000..45319d90b Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_icon_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_pressed.png b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_pressed.png new file mode 100644 index 000000000..f6b49ca9c Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable-xxhdpi/com_facebook_button_like_pressed.png differ diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like.xml b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like.xml new file mode 100644 index 000000000..ff7a04b64 --- /dev/null +++ b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_background.png b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_background.png new file mode 100644 index 000000000..e7723b5a0 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_background.png differ diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_background_selected.png b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_background_selected.png new file mode 100644 index 000000000..68c70bcee Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_background_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_icon.png b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_icon.png new file mode 100644 index 000000000..30a955592 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_icon.png differ diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_icon_selected.png b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_icon_selected.png new file mode 100644 index 000000000..ead35ca29 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_icon_selected.png differ diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_pressed.png b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_pressed.png new file mode 100644 index 000000000..fc5a53534 Binary files /dev/null and b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_pressed.png differ diff --git a/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_selected.xml b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_selected.xml new file mode 100644 index 000000000..042f23ea8 --- /dev/null +++ b/platforms/android/FacebookLib/res/drawable/com_facebook_button_like_selected.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/platforms/android/FacebookLib/res/layout/com_facebook_friendpickerfragment.xml b/platforms/android/FacebookLib/res/layout/com_facebook_friendpickerfragment.xml index b42f15e7d..1fc9f44fa 100644 --- a/platforms/android/FacebookLib/res/layout/com_facebook_friendpickerfragment.xml +++ b/platforms/android/FacebookLib/res/layout/com_facebook_friendpickerfragment.xml @@ -16,16 +16,16 @@ --> + android:layout_width="match_parent" + android:layout_height="match_parent"> @@ -40,13 +40,13 @@ + android:layout_width="match_parent"/> diff --git a/platforms/android/FacebookLib/res/layout/com_facebook_placepickerfragment.xml b/platforms/android/FacebookLib/res/layout/com_facebook_placepickerfragment.xml index 56c5cc8ee..87374c731 100644 --- a/platforms/android/FacebookLib/res/layout/com_facebook_placepickerfragment.xml +++ b/platforms/android/FacebookLib/res/layout/com_facebook_placepickerfragment.xml @@ -16,8 +16,8 @@ --> @@ -26,8 +26,8 @@ @@ -37,7 +37,7 @@ Su contraseña de Facebook ha cambiado, por favor vuelva a conectar la aplicación con Facebook. Se produjo un error al conectar con Facebook, por favor vuelva a conectar la aplicación con Facebook. Una conexión de aplicación con Facebook falló debido a permisos insuficientes, por favor, conceda los permisos de aplicaciones para realizar la operación. + Error al descargar la imagen. diff --git a/platforms/android/FacebookLib/res/values-he/strings.xml b/platforms/android/FacebookLib/res/values-he/strings.xml index 4ab34a3e2..48f29e328 100644 --- a/platforms/android/FacebookLib/res/values-he/strings.xml +++ b/platforms/android/FacebookLib/res/values-he/strings.xml @@ -41,4 +41,5 @@ .ארעה שגיאה בעת החיבור עם פייסבוק, חבר בבקשה מחדש את האפליקציה עם פייסבוק .חיבור האפליקציה עם פייסבוק נכשל בשל הרשאות לא מספקות, אנא הענק הרשאות לאפליקציה בכדי שהפעולה תוכל להתבצע אישור + שגיאה בעת הורדת תמונה diff --git a/platforms/android/FacebookLib/res/values-iw/strings.xml b/platforms/android/FacebookLib/res/values-iw/strings.xml index 4ab34a3e2..48f29e328 100644 --- a/platforms/android/FacebookLib/res/values-iw/strings.xml +++ b/platforms/android/FacebookLib/res/values-iw/strings.xml @@ -41,4 +41,5 @@ .ארעה שגיאה בעת החיבור עם פייסבוק, חבר בבקשה מחדש את האפליקציה עם פייסבוק .חיבור האפליקציה עם פייסבוק נכשל בשל הרשאות לא מספקות, אנא הענק הרשאות לאפליקציה בכדי שהפעולה תוכל להתבצע אישור + שגיאה בעת הורדת תמונה diff --git a/platforms/android/FacebookLib/res/values/attrs.xml b/platforms/android/FacebookLib/res/values/attrs.xml index 21c3bacef..64c365c2d 100644 --- a/platforms/android/FacebookLib/res/values/attrs.xml +++ b/platforms/android/FacebookLib/res/values/attrs.xml @@ -49,4 +49,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/platforms/android/FacebookLib/res/values/strings.xml b/platforms/android/FacebookLib/res/values/strings.xml index e73d31fc0..efa3b79b4 100644 --- a/platforms/android/FacebookLib/res/values/strings.xml +++ b/platforms/android/FacebookLib/res/values/strings.xml @@ -17,6 +17,8 @@ OK + Like + Liked Log out Log in with Facebook Logged in as: %1$s @@ -42,4 +44,5 @@ Please log into this app again to reconnect your Facebook account. This app doesn’t have permission to do this. To change permissions, try logging into the app again. New! You\'re in control - choose what info you want to share with apps. + Unexpected error while downloading an image. diff --git a/platforms/android/FacebookLib/res/values/styles.xml b/platforms/android/FacebookLib/res/values/styles.xml index 926b60346..11c954d48 100644 --- a/platforms/android/FacebookLib/res/values/styles.xml +++ b/platforms/android/FacebookLib/res/values/styles.xml @@ -32,6 +32,28 @@ #FFFFFF 16.0sp + 11.0sp + #6a7180 + 2dp + 6dp + + 8dp + 8dp + 0dp + 0dp + 8dp + #FFFFFF + 13.0sp + + 6dp + 3dp + 3dp + 1dp + #6a7180 + #6a7180 + 11.0sp + 6dp + 50dp 100dp 180dp diff --git a/platforms/android/FacebookLib/src/com/facebook/AppEventsLogger.java b/platforms/android/FacebookLib/src/com/facebook/AppEventsLogger.java index c4f7ed61e..cf3ebf607 100644 --- a/platforms/android/FacebookLib/src/com/facebook/AppEventsLogger.java +++ b/platforms/android/FacebookLib/src/com/facebook/AppEventsLogger.java @@ -85,7 +85,7 @@ * 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 a time threshold is passed (currently 15 seconds).
  • *
  • when an app has gone to background and is then brought back to the foreground.
  • *
*
  • @@ -147,7 +147,7 @@ public enum FlushBehavior { 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 FLUSH_PERIOD_IN_SECONDS = 15; private static final int APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS = 60 * 60 * 24; private static final int FLUSH_APP_SESSION_INFO_IN_SECONDS = 30; @@ -795,7 +795,7 @@ private static void flushAndWait(final FlushReason reason) { try { flushResults = buildAndExecuteRequests(reason, keysToFlush); } catch (Exception e) { - Log.d(TAG, "Caught unexpected exception while flushing: " + e.toString()); + Utility.logd(TAG, "Caught unexpected exception while flushing: ", e); } synchronized (staticLock) { @@ -871,6 +871,10 @@ private static Request buildRequestForSession(final AccessTokenAppIdPair accessT requestParameters.putString("access_token", accessTokenAppId.getAccessToken()); postRequest.setParameters(requestParameters); + if (fetchedAppSettings == null) { + return null; + } + int numEvents = sessionEventsState.populateRequest(postRequest, fetchedAppSettings.supportsImplicitLogging(), fetchedAppSettings.supportsAttribution(), limitEventUsage); if (numEvents == 0) { diff --git a/platforms/android/FacebookLib/src/com/facebook/AuthorizationClient.java b/platforms/android/FacebookLib/src/com/facebook/AuthorizationClient.java index 00a0c0726..3e8e56222 100644 --- a/platforms/android/FacebookLib/src/com/facebook/AuthorizationClient.java +++ b/platforms/android/FacebookLib/src/com/facebook/AuthorizationClient.java @@ -175,7 +175,7 @@ void cancelCurrentHandler() { } boolean onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == pendingRequest.getRequestCode()) { + if (pendingRequest != null && requestCode == pendingRequest.getRequestCode()) { return currentHandler.onActivityResult(requestCode, resultCode, data); } return false; @@ -582,6 +582,9 @@ boolean needsInternetPermission() { @Override void cancel() { if (loginDialog != null) { + // Since we are calling dismiss explicitly, we need to remove the completion listener to prevent + // responding to the upcoming "Cancel" result. + loginDialog.setOnCompleteListener(null); loginDialog.dismiss(); loginDialog = null; } @@ -678,14 +681,12 @@ void onWebDialogComplete(AuthorizationRequest request, Bundle values, private void saveCookieToken(String token) { Context context = getStartActivityDelegate().getActivityContext(); - SharedPreferences sharedPreferences = context.getSharedPreferences( + 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."); - } + Context.MODE_PRIVATE) + .edit() + .putString(WEB_VIEW_AUTH_HANDLER_TOKEN_KEY, token) + .apply(); } private String loadCookieToken() { diff --git a/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java b/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java index 47be1df96..20d25d7ef 100644 --- a/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java +++ b/platforms/android/FacebookLib/src/com/facebook/FacebookSdkVersion.java @@ -17,5 +17,5 @@ package com.facebook; final class FacebookSdkVersion { - public static final String BUILD = "3.18.1"; + public static final String BUILD = "3.20.0"; } diff --git a/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java b/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java index 2a5e2155c..029a8452d 100644 --- a/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java +++ b/platforms/android/FacebookLib/src/com/facebook/FacebookTimeSpentData.java @@ -253,9 +253,9 @@ private void logAppDeactivatedEvent(AppEventsLogger logger, eventParams.putInt( AppEventsConstants.EVENT_NAME_SESSION_INTERRUPTIONS, interruptionCount); - eventParams.putInt( + eventParams.putString( AppEventsConstants.EVENT_NAME_TIME_BETWEEN_SESSIONS, - getQuantaIndex(interruptionDurationMillis)); + String.format("session_quanta_%d", getQuantaIndex(interruptionDurationMillis))); eventParams.putString( AppEventsConstants.EVENT_PARAM_SOURCE_APPLICATION, firstOpenSourceApplication); diff --git a/platforms/android/FacebookLib/src/com/facebook/Request.java b/platforms/android/FacebookLib/src/com/facebook/Request.java index 9b95938c8..32fcfd050 100644 --- a/platforms/android/FacebookLib/src/com/facebook/Request.java +++ b/platforms/android/FacebookLib/src/com/facebook/Request.java @@ -124,6 +124,7 @@ public class Request { private String overriddenURL; private Object tag; private String version; + private boolean skipClientToken = false; /** * Constructs a request without a session, graph path, or any other parameters. @@ -905,6 +906,13 @@ public final void setVersion(String version) { this.version = version; } + /** + * This is an internal function that is not meant to be used by developers. + */ + public final void setSkipClientToken(boolean skipClientToken) { + this.skipClientToken = skipClientToken; + } + /** * Returns the parameters for this request. * @@ -1698,7 +1706,7 @@ private void addCommonParameters() { Logger.registerAccessToken(accessToken); this.parameters.putString(ACCESS_TOKEN_PARAM, accessToken); } - } else if (!this.parameters.containsKey(ACCESS_TOKEN_PARAM)) { + } else if (!skipClientToken && !this.parameters.containsKey(ACCESS_TOKEN_PARAM)) { String appID = Settings.getApplicationId(); String clientToken = Settings.getClientToken(); if (!Utility.isNullOrEmpty(appID) && !Utility.isNullOrEmpty(clientToken)) { @@ -1912,7 +1920,9 @@ final static void serializeToUrlConnection(RequestBatch requests, HttpURLConnect processRequest(requests, logger, numRequests, url, outputStream); } finally { - outputStream.close(); + if (outputStream != null) { + outputStream.close(); + } } logger.log(); diff --git a/platforms/android/FacebookLib/src/com/facebook/RequestAsyncTask.java b/platforms/android/FacebookLib/src/com/facebook/RequestAsyncTask.java index db60a18f9..62a2cf158 100644 --- a/platforms/android/FacebookLib/src/com/facebook/RequestAsyncTask.java +++ b/platforms/android/FacebookLib/src/com/facebook/RequestAsyncTask.java @@ -32,7 +32,6 @@ * 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; @@ -172,18 +171,18 @@ protected List doInBackground(Void... params) { } RequestAsyncTask executeOnSettingsExecutor() { - try { - if (executeOnExecutorMethod != null) { + if (executeOnExecutorMethod != null) { + try { executeOnExecutorMethod.invoke(this, Settings.getExecutor(), null); - return this; + } catch (InvocationTargetException e) { + // fall-through + } catch (IllegalAccessException e) { + // fall-through } - } catch (InvocationTargetException e) { - // fall-through - } catch (IllegalAccessException e) { - // fall-through + } else { + this.execute(); } - this.execute(); return this; } } diff --git a/platforms/android/FacebookLib/src/com/facebook/Session.java b/platforms/android/FacebookLib/src/com/facebook/Session.java index 4fe231168..6711b8837 100644 --- a/platforms/android/FacebookLib/src/com/facebook/Session.java +++ b/platforms/android/FacebookLib/src/com/facebook/Session.java @@ -1672,9 +1672,13 @@ public void onServiceConnected(ComponentName className, IBinder service) { public void onServiceDisconnected(ComponentName arg) { cleanup(); - // We returned an error so there's no point in - // keeping the binding open. - staticContext.unbindService(TokenRefreshRequest.this); + try { + // We returned an error so there's no point in + // keeping the binding open. + staticContext.unbindService(TokenRefreshRequest.this); + } catch (IllegalArgumentException ex) { + // Do nothing, the connection was already unbound + } } private void cleanup() { diff --git a/platforms/android/FacebookLib/src/com/facebook/Settings.java b/platforms/android/FacebookLib/src/com/facebook/Settings.java index c08376c1d..7ab55b2f5 100644 --- a/platforms/android/FacebookLib/src/com/facebook/Settings.java +++ b/platforms/android/FacebookLib/src/com/facebook/Settings.java @@ -20,12 +20,15 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; +import android.util.Base64; import android.util.Log; import com.facebook.android.BuildConfig; import com.facebook.internal.AttributionIdentifiers; @@ -36,6 +39,8 @@ import org.json.JSONObject; import java.lang.reflect.Field; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -61,7 +66,7 @@ public final class Settings { private static volatile String facebookDomain = FACEBOOK_COM; private static AtomicLong onProgressThreshold = new AtomicLong(65536); private static volatile boolean platformCompatibilityEnabled; - private static volatile boolean isLoggingEnabled = BuildConfig.DEBUG; + private static volatile boolean isDebugEnabled = BuildConfig.DEBUG; private static final int DEFAULT_CORE_POOL_SIZE = 5; private static final int DEFAULT_MAXIMUM_POOL_SIZE = 128; @@ -112,6 +117,12 @@ public static synchronized void sdkInitialize(Context context) { if (sdkInitialized == true) { return; } + + // Make sure we've loaded default settings if we haven't already. + Settings.loadDefaultsFromMetadataIfNeeded(context); + // Load app settings from network so that dialog configs are available + Utility.loadAppSettingsAsync(context, Settings.getApplicationId()); + BoltsMeasurementEventListener.getInstance(context.getApplicationContext()); sdkInitialized = true; } @@ -184,24 +195,39 @@ public static final void clearLoggingBehaviors() { */ public static final boolean isLoggingBehaviorEnabled(LoggingBehavior behavior) { synchronized (loggingBehaviors) { - return Settings.isLoggingEnabled() && loggingBehaviors.contains(behavior); + return Settings.isDebugEnabled() && loggingBehaviors.contains(behavior); } } /** - * Indicates if logging is enabled. + * This method is deprecated. Use {@link Settings#isDebugEnabled()} instead. */ + @Deprecated public static final boolean isLoggingEnabled() { - return isLoggingEnabled; + return isDebugEnabled(); } /** - * Used to enable or disable logging, defaults to BuildConfig.DEBUG. - * @param enabled - * Logging is enabled if true, disabled if false. + * This method is deprecated. Use {@link Settings#setIsDebugEnabled(boolean)} instead. */ + @Deprecated public static final void setIsLoggingEnabled(boolean enabled) { - isLoggingEnabled = enabled; + setIsDebugEnabled(enabled); + } + + /** + * Indicates if we are in debug mode. + */ + public static final boolean isDebugEnabled() { + return isDebugEnabled; + } + + /** + * Used to enable or disable logging, and other debug features. Defaults to BuildConfig.DEBUG. + * @param enabled Debug features (like logging) are enabled if true, disabled if false. + */ + public static final void setIsDebugEnabled(boolean enabled) { + isDebugEnabled = enabled; } /** @@ -404,7 +430,7 @@ static Response publishInstallAndWaitForResponse( publishResponse.getGraphObject().getInnerJSONObject() != null) { editor.putString(jsonKey, publishResponse.getGraphObject().getInnerJSONObject().toString()); } - editor.commit(); + editor.apply(); return publishResponse; } @@ -484,10 +510,10 @@ public static boolean getLimitEventAndDataUsage(Context context) { * @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(); + context.getSharedPreferences(APP_EVENT_PREFERENCES, Context.MODE_PRIVATE) + .edit() + .putBoolean("limitEventUsage", limitEventUsage) + .apply(); } /** @@ -567,6 +593,39 @@ static void loadDefaultsFromMetadataIfNeeded(Context context) { } } + public static String getApplicationSignature(Context context) { + if (context == null) { + return null; + } + PackageManager packageManager = context.getPackageManager(); + if (packageManager == null) { + return null; + } + + String packageName = context.getPackageName(); + PackageInfo pInfo; + try { + pInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException e) { + return null; + } + + Signature[] signatures = pInfo.signatures; + if (signatures == null || signatures.length == 0) { + return null; + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + + md.update(pInfo.signatures[0].toByteArray()); + return Base64.encodeToString(md.digest(), Base64.URL_SAFE | Base64.NO_PADDING); + } + /** * Gets the Facebook application ID for the current app. This will be null unless explicitly set or unless * loadDefaultsFromMetadata has been called. diff --git a/platforms/android/FacebookLib/src/com/facebook/SharedPreferencesTokenCachingStrategy.java b/platforms/android/FacebookLib/src/com/facebook/SharedPreferencesTokenCachingStrategy.java index 0286d3c1f..10eca8b80 100644 --- a/platforms/android/FacebookLib/src/com/facebook/SharedPreferencesTokenCachingStrategy.java +++ b/platforms/android/FacebookLib/src/com/facebook/SharedPreferencesTokenCachingStrategy.java @@ -163,18 +163,14 @@ public void save(Bundle bundle) { return; } } - - boolean successfulCommit = editor.commit(); - if (!successfulCommit) { - Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "SharedPreferences.Editor.commit() was not successful"); - } + editor.apply(); } /** * Clears out all token information stored in this cache. */ public void clear() { - cache.edit().clear().commit(); + cache.edit().clear().apply(); } private void serializeKey(String key, Bundle bundle, SharedPreferences.Editor editor) diff --git a/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java b/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java index 8d5b73024..3ebf61c98 100644 --- a/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java +++ b/platforms/android/FacebookLib/src/com/facebook/UiLifecycleHelper.java @@ -24,7 +24,10 @@ import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; +import com.facebook.internal.LikeActionController; import com.facebook.internal.NativeProtocol; +import com.facebook.internal.PendingCallStore; +import com.facebook.internal.Utility; import com.facebook.widget.FacebookDialog; import java.util.UUID; @@ -42,7 +45,7 @@ * results generated by those dialogs. */ public class UiLifecycleHelper { - private static final String DIALOG_CALL_BUNDLE_SAVE_KEY = + private static final String DIALOG_CALL_ID_SAVE_KEY = "com.facebook.UiLifecycleHelper.pendingFacebookDialogCallKey"; private final static String ACTIVITY_NULL_MESSAGE = "activity cannot be null"; @@ -52,7 +55,9 @@ public class UiLifecycleHelper { private final BroadcastReceiver receiver; private final LocalBroadcastManager broadcastManager; // Members related to handling FacebookDialog calls - private FacebookDialog.PendingCall pendingFacebookDialogCall; + private UUID pendingFacebookDialogCallId; + private PendingCallStore pendingFacebookDialogCallStore; + private AppEventsLogger appEventsLogger; /** @@ -71,10 +76,10 @@ public UiLifecycleHelper(Activity activity, Session.StatusCallback callback) { this.callback = callback; this.receiver = new ActiveSessionBroadcastReceiver(); this.broadcastManager = LocalBroadcastManager.getInstance(activity); + this.pendingFacebookDialogCallStore = PendingCallStore.getInstance(); + // initialize SDK Settings.sdkInitialize(activity); - // Make sure we've loaded default settings if we haven't already. - Settings.loadDefaultsFromMetadataIfNeeded(activity); } /** @@ -94,7 +99,11 @@ public void onCreate(Bundle savedInstanceState) { Session.setActiveSession(session); } if (savedInstanceState != null) { - pendingFacebookDialogCall = savedInstanceState.getParcelable(DIALOG_CALL_BUNDLE_SAVE_KEY); + String callIdString = savedInstanceState.getString(DIALOG_CALL_ID_SAVE_KEY); + if (callIdString != null) { + pendingFacebookDialogCallId = UUID.fromString(callIdString); + } + pendingFacebookDialogCallStore.restoreFromSavedInstanceState(savedInstanceState); } } @@ -140,7 +149,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { * @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 + * @param facebookDialogCallback the callback for handling FacebookDialog results, can be null */ public void onActivityResult(int requestCode, int resultCode, Intent data, FacebookDialog.Callback facebookDialogCallback) { @@ -149,6 +158,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data, session.onActivityResult(activity, requestCode, resultCode, data); } + if (LikeActionController.handleOnActivityResult(activity, requestCode, resultCode, data)) { + return; + } + handleFacebookDialogActivityResult(requestCode, resultCode, data, facebookDialogCallback); } @@ -159,7 +172,10 @@ public void onActivityResult(int requestCode, int resultCode, Intent data, */ public void onSaveInstanceState(Bundle outState) { Session.saveSession(Session.getActiveSession(), outState); - outState.putParcelable(DIALOG_CALL_BUNDLE_SAVE_KEY, pendingFacebookDialogCall); + if (pendingFacebookDialogCallId != null) { + outState.putString(DIALOG_CALL_ID_SAVE_KEY, pendingFacebookDialogCallId.toString()); + } + pendingFacebookDialogCallStore.saveInstanceState(outState); } /** @@ -198,12 +214,15 @@ public void onDestroy() { * @param appCall an PendingCall object containing the call ID */ public void trackPendingDialogCall(FacebookDialog.PendingCall pendingCall) { - if (pendingFacebookDialogCall != null) { + if (pendingFacebookDialogCallId != 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; + if (pendingCall != null){ + pendingFacebookDialogCallId = pendingCall.getCallId(); + pendingFacebookDialogCallStore.trackPendingCall(pendingCall); + } } /** @@ -258,7 +277,12 @@ public void onReceive(Context context, Intent intent) { private boolean handleFacebookDialogActivityResult(int requestCode, int resultCode, Intent data, FacebookDialog.Callback facebookDialogCallback) { - if (pendingFacebookDialogCall == null || pendingFacebookDialogCall.getRequestCode() != requestCode) { + if (pendingFacebookDialogCallId == null) { + return false; + } + FacebookDialog.PendingCall pendingCall = + pendingFacebookDialogCallStore.getPendingCallById(pendingFacebookDialogCallId); + if (pendingCall == null || pendingCall.getRequestCode() != requestCode) { return false; } @@ -272,9 +296,9 @@ private boolean handleFacebookDialogActivityResult(int requestCode, int resultCo UUID callId = NativeProtocol.getCallIdFromIntent(data); // Was this result for the call we are waiting on? - if (callId != null && pendingFacebookDialogCall.getCallId().equals(callId)) { + if (callId != null && pendingFacebookDialogCallId.equals(callId)) { // Yes, we can handle it normally. - FacebookDialog.handleActivityResult(activity, pendingFacebookDialogCall, requestCode, data, + FacebookDialog.handleActivityResult(activity, pendingCall, requestCode, data, facebookDialogCallback); } else { // No, send a cancellation error to the pending call and ignore the result, because we @@ -282,13 +306,23 @@ private boolean handleFacebookDialogActivityResult(int requestCode, int resultCo cancelPendingAppCall(facebookDialogCallback); } - pendingFacebookDialogCall = null; + stopTrackingPendingAppCall(); return true; } private void cancelPendingAppCall(FacebookDialog.Callback facebookDialogCallback) { + if (pendingFacebookDialogCallId == null) { + return; + } + + FacebookDialog.PendingCall pendingCall = + pendingFacebookDialogCallStore.getPendingCallById(pendingFacebookDialogCallId); + if (pendingCall == null) { + return; + } + if (facebookDialogCallback != null) { - Intent pendingIntent = pendingFacebookDialogCall.getRequestIntent(); + Intent pendingIntent = pendingCall.getRequestIntent(); Intent cancelIntent = new Intent(); cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID, @@ -299,9 +333,15 @@ private void cancelPendingAppCall(FacebookDialog.Callback facebookDialogCallback 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); + FacebookDialog.handleActivityResult(activity, pendingCall, + pendingCall.getRequestCode(), cancelIntent, facebookDialogCallback); } - pendingFacebookDialogCall = null; + + stopTrackingPendingAppCall(); + } + + private void stopTrackingPendingAppCall() { + pendingFacebookDialogCallStore.stopTrackingPendingCall(pendingFacebookDialogCallId); + pendingFacebookDialogCallId = null; } } diff --git a/platforms/android/FacebookLib/src/com/facebook/android/Facebook.java b/platforms/android/FacebookLib/src/com/facebook/android/Facebook.java index 4680f364d..0c9c0aa51 100644 --- a/platforms/android/FacebookLib/src/com/facebook/android/Facebook.java +++ b/platforms/android/FacebookLib/src/com/facebook/android/Facebook.java @@ -531,9 +531,13 @@ public void onServiceConnected(ComponentName className, IBinder service) { @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); + try { + // We returned an error so there's no point in + // keeping the binding open. + applicationsContext.unbindService(TokenRefreshServiceConnection.this); + } catch (IllegalArgumentException ex) { + // Do nothing, the connection was already unbound + } } private void refreshToken() { diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/AnalyticsEvents.java b/platforms/android/FacebookLib/src/com/facebook/internal/AnalyticsEvents.java index e57ec0fb7..a7b8038ee 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/AnalyticsEvents.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/AnalyticsEvents.java @@ -19,6 +19,7 @@ public class AnalyticsEvents { 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_CALL_ID = "call_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 = @@ -36,4 +37,22 @@ public class AnalyticsEvents { 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"; + public static final String EVENT_NATIVE_DIALOG_TYPE_LIKE = "fb_dialogs_present_like"; + + public static final String EVENT_LIKE_VIEW_CANNOT_PRESENT_DIALOG = "fb_like_control_cannot_present_dialog"; + public static final String EVENT_LIKE_VIEW_DID_LIKE = "fb_like_control_did_like"; + public static final String EVENT_LIKE_VIEW_DID_PRESENT_DIALOG = "fb_like_control_did_present_dialog"; + public static final String EVENT_LIKE_VIEW_DID_PRESENT_FALLBACK = "fb_like_control_did_present_fallback_dialog"; + public static final String EVENT_LIKE_VIEW_DID_TAP = "fb_like_control_did_tap"; + public static final String EVENT_LIKE_VIEW_DID_UNLIKE = "fb_like_control_did_unlike"; + public static final String EVENT_LIKE_VIEW_DID_UNDO_QUICKLY = "fb_like_control_did_undo_quickly"; + public static final String EVENT_LIKE_VIEW_DIALOG_DID_SUCCEED = "fb_like_control_dialog_did_succeed"; + public static final String EVENT_LIKE_VIEW_ERROR = "fb_like_control_error"; + + public static final String PARAMETER_LIKE_VIEW_STYLE = "style"; + public static final String PARAMETER_LIKE_VIEW_AUXILIARY_POSITION = "auxiliary_position"; + public static final String PARAMETER_LIKE_VIEW_HORIZONTAL_ALIGNMENT = "horizontal_alignment"; + public static final String PARAMETER_LIKE_VIEW_OBJECT_ID = "object_id"; + public static final String PARAMETER_LIKE_VIEW_CURRENT_ACTION = "current_action"; + public static final String PARAMETER_LIKE_VIEW_ERROR_JSON = "error"; } diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java b/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java index 4efe7a3e6..c20682526 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/AttributionIdentifiers.java @@ -113,7 +113,7 @@ public static AttributionIdentifiers getAttributionIdentifiers(Context context) 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; + return identifiers; } int attributionColumnIndex = c.getColumnIndex(ATTRIBUTION_ID_COLUMN_NAME); int androidIdColumnIndex = c.getColumnIndex(ANDROID_ID_COLUMN_NAME); @@ -149,4 +149,4 @@ public String getAndroidAdvertiserId() { public boolean isTrackingLimited() { return limitTracking; } -} \ No newline at end of file +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/BundleJSONConverter.java b/platforms/android/FacebookLib/src/com/facebook/internal/BundleJSONConverter.java new file mode 100644 index 000000000..c674ff3c1 --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/BundleJSONConverter.java @@ -0,0 +1,199 @@ +/** + * 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.os.Bundle; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +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. + */ + +/** + * A helper class that can round trip between JSON and Bundle objects that contains the types: + * Boolean, Integer, Long, Double, String + * If other types are found, an IllegalArgumentException is thrown. + */ +public class BundleJSONConverter { + private static final Map, Setter> SETTERS = new HashMap, Setter>(); + + static { + SETTERS.put(Boolean.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putBoolean(key, (Boolean) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Integer.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putInt(key, (Integer) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Long.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putLong(key, (Long) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(Double.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putDouble(key, (Double) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(String.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + bundle.putString(key, (String) value); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + json.put(key, value); + } + }); + SETTERS.put(String[].class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + throw new IllegalArgumentException("Unexpected type from JSON"); + } + + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (String stringValue : (String[])value) { + jsonArray.put(stringValue); + } + json.put(key, jsonArray); + } + }); + + SETTERS.put(JSONArray.class, new Setter() { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { + JSONArray jsonArray = (JSONArray)value; + ArrayList stringArrayList = new ArrayList(); + // Empty list, can't even figure out the type, assume an ArrayList + if (jsonArray.length() == 0) { + bundle.putStringArrayList(key, stringArrayList); + return; + } + + // Only strings are supported for now + for (int i = 0; i < jsonArray.length(); i++) { + Object current = jsonArray.get(i); + if (current instanceof String) { + stringArrayList.add((String)current); + } else { + throw new IllegalArgumentException("Unexpected type in an array: " + current.getClass()); + } + } + bundle.putStringArrayList(key, stringArrayList); + } + + @Override + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException { + throw new IllegalArgumentException("JSONArray's are not supported in bundles."); + } + }); + } + + public interface Setter { + public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException; + public void setOnJSON(JSONObject json, String key, Object value) throws JSONException; + } + + public static JSONObject convertToJSON(Bundle bundle) throws JSONException { + JSONObject json = new JSONObject(); + + for(String key : bundle.keySet()) { + Object value = bundle.get(key); + if (value == null) { + // Null is not supported. + continue; + } + + // Special case List as getClass would not work, since List is an interface + if (value instanceof List) { + JSONArray jsonArray = new JSONArray(); + @SuppressWarnings("unchecked") + List listValue = (List)value; + for (String stringValue : listValue) { + jsonArray.put(stringValue); + } + json.put(key, jsonArray); + continue; + } + + // Special case Bundle as it's one way, on the return it will be JSONObject + if (value instanceof Bundle) { + json.put(key, convertToJSON((Bundle)value)); + continue; + } + + Setter setter = SETTERS.get(value.getClass()); + if (setter == null) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass()); + } + setter.setOnJSON(json, key, value); + } + + return json; + } + + public static Bundle convertToBundle(JSONObject jsonObject) throws JSONException { + Bundle bundle = new Bundle(); + @SuppressWarnings("unchecked") + Iterator jsonIterator = jsonObject.keys(); + while (jsonIterator.hasNext()) { + String key = jsonIterator.next(); + Object value = jsonObject.get(key); + if (value == null || value == JSONObject.NULL) { + // Null is not supported. + continue; + } + + // Special case JSONObject as it's one way, on the return it would be Bundle. + if (value instanceof JSONObject) { + bundle.putBundle(key, convertToBundle((JSONObject)value)); + continue; + } + + Setter setter = SETTERS.get(value.getClass()); + if (setter == null) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass()); + } + setter.setOnBundle(bundle, key, value); + } + + return bundle; + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/FacebookWebFallbackDialog.java b/platforms/android/FacebookLib/src/com/facebook/internal/FacebookWebFallbackDialog.java new file mode 100644 index 000000000..2a2f4b603 --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/FacebookWebFallbackDialog.java @@ -0,0 +1,174 @@ +/** + * 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.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.webkit.WebView; +import com.facebook.FacebookException; +import com.facebook.android.Util; +import com.facebook.widget.FacebookDialog; +import com.facebook.widget.WebDialog; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.EnumSet; + +/** + * 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. + * + * This dialog is used as a fallback when a native FacebookDialog could not be displayed. The primary reason for + * this separation is to keep this approach for internal use only until we stabilize the API. + */ +public class FacebookWebFallbackDialog extends WebDialog { + private static final String TAG = FacebookWebFallbackDialog.class.getName(); + private static final int OS_BACK_BUTTON_RESPONSE_TIMEOUT_MILLISECONDS = 1500; + + private boolean waitingForDialogToClose; + + public static boolean presentWebFallback(final Context context, + String dialogUrl, + String applicationId, + final FacebookDialog.PendingCall appCall, + final FacebookDialog.Callback callback) { + if (Utility.isNullOrEmpty(dialogUrl)) { + return false; + } + + String redirectUrl = String.format("fb%s://bridge/", applicationId); + + // Show the webdialog. + FacebookWebFallbackDialog fallbackWebDialog = new FacebookWebFallbackDialog( + context, dialogUrl, redirectUrl); + fallbackWebDialog.setOnCompleteListener(new WebDialog.OnCompleteListener() { + @Override + public void onComplete(Bundle values, FacebookException error) { + Intent dummyIntent = new Intent(); + dummyIntent.putExtras(values == null ? new Bundle() : values); + FacebookDialog.handleActivityResult( + context, + appCall, + appCall.getRequestCode(), + dummyIntent, + callback); + } + }); + + fallbackWebDialog.show(); + return true; + } + + private FacebookWebFallbackDialog(Context context, String url, String expectedRedirectUrl) { + super(context, url); + + setExpectedRedirectUrl(expectedRedirectUrl); + } + + @Override + protected Bundle parseResponseUri(String url) { + Uri responseUri = Uri.parse(url); + Bundle queryParams = Utility.parseUrlQueryString(responseUri.getQuery()); + + // Convert Bridge args to the format that the Native dialog code understands. + String bridgeArgsJSONString = queryParams.getString(ServerProtocol.FALLBACK_DIALOG_PARAM_BRIDGE_ARGS); + queryParams.remove(ServerProtocol.FALLBACK_DIALOG_PARAM_BRIDGE_ARGS); + + if (!Utility.isNullOrEmpty(bridgeArgsJSONString)) { + Bundle bridgeArgs; + try { + JSONObject bridgeArgsJSON = new JSONObject(bridgeArgsJSONString); + bridgeArgs = BundleJSONConverter.convertToBundle(bridgeArgsJSON); + queryParams.putBundle(NativeProtocol.EXTRA_PROTOCOL_BRIDGE_ARGS, bridgeArgs); + } catch (JSONException je) { + Utility.logd(TAG, "Unable to parse bridge_args JSON", je); + } + } + + // Convert Method results to the format that the Native dialog code understands. + String methodResultsJSONString = queryParams.getString(ServerProtocol.FALLBACK_DIALOG_PARAM_METHOD_RESULTS); + queryParams.remove(ServerProtocol.FALLBACK_DIALOG_PARAM_METHOD_RESULTS); + + if (!Utility.isNullOrEmpty(methodResultsJSONString)) { + methodResultsJSONString = Utility.isNullOrEmpty(methodResultsJSONString) ? "{}" : methodResultsJSONString; + Bundle methodResults; + try { + JSONObject methodArgsJSON = new JSONObject(methodResultsJSONString); + methodResults = BundleJSONConverter.convertToBundle(methodArgsJSON); + queryParams.putBundle(NativeProtocol.EXTRA_PROTOCOL_METHOD_RESULTS, methodResults); + } catch (JSONException je) { + Utility.logd(TAG, "Unable to parse bridge_args JSON", je); + } + } + + // The web host does not send a numeric version back. Put the latest known version in there so NativeProtocol + // can continue parsing the response. + queryParams.remove(ServerProtocol.FALLBACK_DIALOG_PARAM_VERSION); + queryParams.putInt(NativeProtocol.EXTRA_PROTOCOL_VERSION, NativeProtocol.getLatestKnownVersion()); + + return queryParams; + } + + @Override + public void dismiss() { + WebView webView = getWebView(); + + if (isListenerCalled() || webView == null || !webView.isShown()) { + // If the listener has been called, or if the WebView isn't visible, we cannot give the dialog a chance + // to respond. So defer to the parent implementation. + super.dismiss(); + return; + } + + // If we have already notified the dialog to close, then ignore this request to dismiss. The timer will + // honor the request. + if (waitingForDialogToClose) { + return; + } + waitingForDialogToClose = true; + + // Now fire off the event that will tell the dialog to wind down. + String eventJS = + "(function() {" + + " var event = document.createEvent('Event');" + + " event.initEvent('fbPlatformDialogMustClose',true,true);" + + " document.dispatchEvent(event);" + + "})();"; + webView.loadUrl("javascript:" + eventJS); + + // Set up a timeout for the dialog to respond. If the timer expires, we need to honor the user's desire to + // dismiss the dialog. + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed( + new Runnable() { + @Override + public void run() { + if (!isListenerCalled()) { + // If we get here, then the dialog did not close quickly enough. So we need to honor the user's + // wish to cancel. + sendCancelToListener(); + } + } + }, + OS_BACK_BUTTON_RESPONSE_TIMEOUT_MILLISECONDS); + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java b/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java index 69bea7f4a..058c55bc8 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/ImageDownloader.java @@ -22,6 +22,7 @@ import android.os.Handler; import android.os.Looper; import com.facebook.FacebookException; +import com.facebook.android.R; import java.io.IOException; import java.io.InputStream; @@ -246,15 +247,19 @@ private static void download(RequestKey key, Context context) { 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); + if (stream != null) { + InputStreamReader reader = new InputStreamReader(stream); + char[] buffer = new char[128]; + int bufferLength; + while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) { + errorMessageBuilder.append(buffer, 0, bufferLength); + } + Utility.closeQuietly(reader); + } else { + errorMessageBuilder.append( + context.getString(R.string.com_facebook_image_download_unknown_error)); } - Utility.closeQuietly(reader); - error = new FacebookException(errorMessageBuilder.toString()); break; } diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/LikeActionController.java b/platforms/android/FacebookLib/src/com/facebook/internal/LikeActionController.java new file mode 100644 index 000000000..d16af520d --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/LikeActionController.java @@ -0,0 +1,1404 @@ +/** + * 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.app.Activity; +import android.content.*; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.v4.content.LocalBroadcastManager; +import android.util.Log; +import com.facebook.*; +import com.facebook.widget.FacebookDialog; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.UUID; +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 class LikeActionController { + + public static final String ACTION_LIKE_ACTION_CONTROLLER_UPDATED = "com.facebook.sdk.LikeActionController.UPDATED"; + public static final String ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR = "com.facebook.sdk.LikeActionController.DID_ERROR"; + public static final String ACTION_LIKE_ACTION_CONTROLLER_DID_RESET = "com.facebook.sdk.LikeActionController.DID_RESET"; + + public static final String ACTION_OBJECT_ID_KEY = "com.facebook.sdk.LikeActionController.OBJECT_ID"; + + public static final String ERROR_INVALID_OBJECT_ID = "Invalid Object Id"; + + private static final String TAG = LikeActionController.class.getSimpleName(); + + private static final int LIKE_ACTION_CONTROLLER_VERSION = 2; + private static final int MAX_CACHE_SIZE = 128; + // MAX_OBJECT_SUFFIX basically accommodates for 1000 session-state changes before the async disk-cache-clear + // finishes. The value is reasonably arbitrary. + private static final int MAX_OBJECT_SUFFIX = 1000; + + private static final String LIKE_ACTION_CONTROLLER_STORE = "com.facebook.LikeActionController.CONTROLLER_STORE_KEY"; + private static final String LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY = "PENDING_CONTROLLER_KEY"; + private static final String LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY = "OBJECT_SUFFIX"; + + private static final String JSON_INT_VERSION_KEY = "com.facebook.internal.LikeActionController.version"; + private static final String JSON_STRING_OBJECT_ID_KEY = "object_id"; + private static final String JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY = "like_count_string_with_like"; + private static final String JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY = "like_count_string_without_like"; + private static final String JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY = "social_sentence_with_like"; + private static final String JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY = "social_sentence_without_like"; + private static final String JSON_BOOL_IS_OBJECT_LIKED_KEY = "is_object_liked"; + private static final String JSON_STRING_UNLIKE_TOKEN_KEY = "unlike_token"; + private static final String JSON_STRING_PENDING_CALL_ID_KEY = "pending_call_id"; + private static final String JSON_BUNDLE_PENDING_CALL_ANALYTICS_BUNDLE = "pending_call_analytics_bundle"; + + private static final String LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY = "object_is_liked"; + private static final String LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY = "like_count_string"; + private static final String LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY = "social_sentence"; + private static final String LIKE_DIALOG_RESPONSE_UNLIKE_TOKEN_KEY = "unlike_token"; + + private static final int ERROR_CODE_OBJECT_ALREADY_LIKED = 3501; + + private static FileLruCache controllerDiskCache; + private static final ConcurrentHashMap cache = + new ConcurrentHashMap(); + private static WorkQueue mruCacheWorkQueue = new WorkQueue(1); // This MUST be 1 for proper synchronization + private static WorkQueue diskIOWorkQueue = new WorkQueue(1); // This MUST be 1 for proper synchronization + private static Handler handler; + private static String objectIdForPendingController; + private static boolean isPendingBroadcastReset; + private static boolean isInitialized; + private static volatile int objectSuffix; + + private Session session; + private Context context; + private String objectId; + private boolean isObjectLiked; + private String likeCountStringWithLike; + private String likeCountStringWithoutLike; + private String socialSentenceWithLike; + private String socialSentenceWithoutLike; + private String unlikeToken; + + private String verifiedObjectId; + private boolean objectIsPage; + private boolean isObjectLikedOnServer; + + private boolean isPendingLikeOrUnlike; + + private UUID pendingCallId; + + private Bundle pendingCallAnalyticsBundle; + + private AppEventsLogger appEventsLogger; + + /** + * Called from UiLifecycleHelper to process any pending likes that had resulted in the Like dialog + * being displayed + * + * @param context Hosting context + * @param requestCode From the originating call to onActivityResult + * @param resultCode From the originating call to onActivityResult + * @param data From the originating call to onActivityResult + * @return Indication of whether the Intent was handled + */ + public static boolean handleOnActivityResult(Context context, + final int requestCode, + final int resultCode, + final Intent data) { + final UUID callId = NativeProtocol.getCallIdFromIntent(data); + if (callId == null) { + return false; + } + + // See if we were waiting on a Like dialog completion. + if (Utility.isNullOrEmpty(objectIdForPendingController)) { + SharedPreferences sharedPreferences = context.getSharedPreferences( + LIKE_ACTION_CONTROLLER_STORE, + Context.MODE_PRIVATE); + + objectIdForPendingController = sharedPreferences.getString( + LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY, + null); + } + + if (Utility.isNullOrEmpty(objectIdForPendingController)) { + // Doesn't look like we were waiting on a Like dialog completion + return false; + } + + getControllerForObjectId( + context, + objectIdForPendingController, + new CreationCallback() { + @Override + public void onComplete(LikeActionController likeActionController) { + likeActionController.onActivityResult(requestCode, resultCode, data, callId); + } + }); + + return true; + } + + /** + * Called by the LikeView when an object-id is set on it. + * @param context context + * @param objectId Object Id + * @return A LikeActionController for the specified object id + */ + public static void getControllerForObjectId( + Context context, + String objectId, + CreationCallback callback) { + if (!isInitialized) { + performFirstInitialize(context); + } + + LikeActionController controllerForObject = getControllerFromInMemoryCache(objectId); + if (controllerForObject != null) { + // Direct object-cache hit + invokeCallbackWithController(callback, controllerForObject); + } else { + diskIOWorkQueue.addActiveWorkItem(new CreateLikeActionControllerWorkItem(context, objectId, callback)); + } + } + + /** + * NOTE: This MUST be called ONLY via the CreateLikeActionControllerWorkItem class to ensure that it happens on the + * right thread, at the right time. + */ + private static void createControllerForObjectId( + Context context, + String objectId, + CreationCallback callback) { + // Check again to see if the controller was created before attempting to deserialize/create one. + // Need to check this in the case where multiple LikeViews are looking for a controller for the same object + // and all got queued up to create one. We only want the first one to go through with the creation, and the + // rest should get the same instance from the object-cache. + LikeActionController controllerForObject = getControllerFromInMemoryCache(objectId); + if (controllerForObject != null) { + // Direct object-cache hit + invokeCallbackWithController(callback, controllerForObject); + return; + } + + // Try deserialize from disk + controllerForObject = deserializeFromDiskSynchronously(context, objectId); + + if (controllerForObject == null) { + controllerForObject = new LikeActionController(context, Session.getActiveSession(), objectId); + serializeToDiskAsync(controllerForObject); + } + + // Update object-cache. + putControllerInMemoryCache(objectId, controllerForObject); + + // Refresh the controller on the Main thread. + final LikeActionController controllerToRefresh = controllerForObject; + handler.post(new Runnable() { + @Override + public void run() { + controllerToRefresh.refreshStatusAsync(); + } + }); + + invokeCallbackWithController(callback, controllerToRefresh); + } + + private synchronized static void performFirstInitialize(Context context) { + if (isInitialized) { + return; + } + + handler = new Handler(Looper.getMainLooper()); + + SharedPreferences sharedPreferences = context.getSharedPreferences( + LIKE_ACTION_CONTROLLER_STORE, + Context.MODE_PRIVATE); + + objectSuffix = sharedPreferences.getInt(LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY, 1); + controllerDiskCache = new FileLruCache(context, TAG, new FileLruCache.Limits()); + + registerSessionBroadcastReceivers(context); + + isInitialized = true; + } + + private static void invokeCallbackWithController(final CreationCallback callback, final LikeActionController controller) { + if (callback == null) { + return; + } + + handler.post(new Runnable() { + @Override + public void run() { + callback.onComplete(controller); + } + }); + } + + // + // In-memory mru-caching code + // + + private static void registerSessionBroadcastReceivers(Context context) { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(context); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Session.ACTION_ACTIVE_SESSION_UNSET); + filter.addAction(Session.ACTION_ACTIVE_SESSION_CLOSED); + filter.addAction(Session.ACTION_ACTIVE_SESSION_OPENED); + + broadcastManager.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context receiverContext, Intent intent) { + if (isPendingBroadcastReset) { + return; + } + + String action = intent.getAction(); + final boolean shouldClearDisk = + Utility.areObjectsEqual(Session.ACTION_ACTIVE_SESSION_UNSET, action) || + Utility.areObjectsEqual(Session.ACTION_ACTIVE_SESSION_CLOSED, action); + + + isPendingBroadcastReset = true; + // Delaying sending the broadcast to reset, because we might get many successive calls from Session + // (to UNSET, SET & OPEN) and a delay would prevent excessive chatter. + final Context broadcastContext = receiverContext; + handler.postDelayed(new Runnable() { + @Override + public void run() { + // Bump up the objectSuffix so that we don't have a filename collision between a cache-clear and + // and a cache-read/write. + // + // NOTE: We know that onReceive() was called on the main thread. This means that even this code + // is running on the main thread, and therefore, there aren't synchronization issues with + // incrementing the objectSuffix and clearing the caches here. + if (shouldClearDisk) { + objectSuffix = (objectSuffix + 1) % MAX_OBJECT_SUFFIX; + broadcastContext.getSharedPreferences(LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE) + .edit() + .putInt(LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY, objectSuffix) + .apply(); + + // Only clearing the actual caches. The MRU index will self-clean with usage. + // Clearing the caches is necessary to prevent leaking like-state across sessions. + cache.clear(); + controllerDiskCache.clearCache(); + } + + broadcastAction(broadcastContext, null, ACTION_LIKE_ACTION_CONTROLLER_DID_RESET); + isPendingBroadcastReset = false; + } + }, 100); + } + }, filter); + } + + private static void putControllerInMemoryCache(String objectId, LikeActionController controllerForObject) { + String cacheKey = getCacheKeyForObjectId(objectId); + // Move this object to the front. Also trim cache if necessary + mruCacheWorkQueue.addActiveWorkItem(new MRUCacheWorkItem(cacheKey, true)); + + cache.put(cacheKey, controllerForObject); + } + + private static LikeActionController getControllerFromInMemoryCache(String objectId) { + String cacheKey = getCacheKeyForObjectId(objectId); + + LikeActionController controller = cache.get(cacheKey); + if (controller != null) { + // Move this object to the front + mruCacheWorkQueue.addActiveWorkItem(new MRUCacheWorkItem(cacheKey, false)); + } + + return controller; + } + + // + // Disk caching code + // + + private static void serializeToDiskAsync(LikeActionController controller) { + String controllerJson = serializeToJson(controller); + String cacheKey = getCacheKeyForObjectId(controller.objectId); + + if (!Utility.isNullOrEmpty(controllerJson) && !Utility.isNullOrEmpty(cacheKey)) { + diskIOWorkQueue.addActiveWorkItem(new SerializeToDiskWorkItem(cacheKey, controllerJson)); + } + } + + /** + * NOTE: This MUST be called ONLY via the SerializeToDiskWorkItem class to ensure that it happens on the + * right thread, at the right time. + */ + private static void serializeToDiskSynchronously(String cacheKey, String controllerJson) { + OutputStream outputStream = null; + try { + outputStream = controllerDiskCache.openPutStream(cacheKey); + outputStream.write(controllerJson.getBytes()); + } catch (IOException e) { + Log.e(TAG, "Unable to serialize controller to disk", e); + } finally { + if (outputStream != null) { + Utility.closeQuietly(outputStream); + } + } + } + + /** + * NOTE: This MUST be called ONLY via the CreateLikeActionControllerWorkItem class to ensure that it happens on the + * right thread, at the right time. + */ + private static LikeActionController deserializeFromDiskSynchronously( + Context context, + String objectId) { + LikeActionController controller = null; + + InputStream inputStream = null; + try { + String cacheKey = getCacheKeyForObjectId(objectId); + inputStream = controllerDiskCache.get(cacheKey); + if (inputStream != null) { + String controllerJsonString = Utility.readStreamToString(inputStream); + if (!Utility.isNullOrEmpty(controllerJsonString)) { + controller = deserializeFromJson(context, controllerJsonString); + } + } + } catch (IOException e) { + Log.e(TAG, "Unable to deserialize controller from disk", e); + controller = null; + } finally { + if (inputStream != null) { + Utility.closeQuietly(inputStream); + } + } + + return controller; + } + + private static LikeActionController deserializeFromJson(Context context, String controllerJsonString) { + LikeActionController controller; + + try { + JSONObject controllerJson = new JSONObject(controllerJsonString); + int version = controllerJson.optInt(JSON_INT_VERSION_KEY, -1); + if (version != LIKE_ACTION_CONTROLLER_VERSION) { + // Don't attempt to deserialize a controller that might be serialized differently than expected. + return null; + } + + controller = new LikeActionController( + context, + Session.getActiveSession(), + controllerJson.getString(JSON_STRING_OBJECT_ID_KEY)); + + // Make sure to default to null and not empty string, to keep the logic elsewhere functioning properly. + controller.likeCountStringWithLike = controllerJson.optString(JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY, null) ; + controller.likeCountStringWithoutLike = controllerJson.optString(JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY, null) ; + controller.socialSentenceWithLike = controllerJson.optString(JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY, null); + controller.socialSentenceWithoutLike = controllerJson.optString(JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY, null); + controller.isObjectLiked = controllerJson.optBoolean(JSON_BOOL_IS_OBJECT_LIKED_KEY); + controller.unlikeToken = controllerJson.optString(JSON_STRING_UNLIKE_TOKEN_KEY, null); + String pendingCallIdString = controllerJson.optString(JSON_STRING_PENDING_CALL_ID_KEY, null); + if (!Utility.isNullOrEmpty(pendingCallIdString)) { + controller.pendingCallId = UUID.fromString(pendingCallIdString); + } + + JSONObject analyticsJSON = controllerJson.optJSONObject(JSON_BUNDLE_PENDING_CALL_ANALYTICS_BUNDLE); + if (analyticsJSON != null) { + controller.pendingCallAnalyticsBundle = BundleJSONConverter.convertToBundle(analyticsJSON); + } + } catch (JSONException e) { + Log.e(TAG, "Unable to deserialize controller from JSON", e); + controller = null; + } + + return controller; + } + + private static String serializeToJson(LikeActionController controller) { + JSONObject controllerJson = new JSONObject(); + try { + controllerJson.put(JSON_INT_VERSION_KEY, LIKE_ACTION_CONTROLLER_VERSION); + controllerJson.put(JSON_STRING_OBJECT_ID_KEY, controller.objectId); + controllerJson.put(JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY, controller.likeCountStringWithLike); + controllerJson.put(JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY, controller.likeCountStringWithoutLike); + controllerJson.put(JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY, controller.socialSentenceWithLike); + controllerJson.put(JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY, controller.socialSentenceWithoutLike); + controllerJson.put(JSON_BOOL_IS_OBJECT_LIKED_KEY, controller.isObjectLiked); + controllerJson.put(JSON_STRING_UNLIKE_TOKEN_KEY, controller.unlikeToken); + if (controller.pendingCallId != null) { + controllerJson.put(JSON_STRING_PENDING_CALL_ID_KEY, controller.pendingCallId.toString()); + } + if (controller.pendingCallAnalyticsBundle != null) { + JSONObject analyticsJSON = BundleJSONConverter.convertToJSON(controller.pendingCallAnalyticsBundle); + if (analyticsJSON != null) { + controllerJson.put(JSON_BUNDLE_PENDING_CALL_ANALYTICS_BUNDLE, analyticsJSON); + } + } + } catch (JSONException e) { + Log.e(TAG, "Unable to serialize controller to JSON", e); + return null; + } + + return controllerJson.toString(); + } + + private static String getCacheKeyForObjectId(String objectId) { + String accessTokenPortion = null; + Session activeSession = Session.getActiveSession(); + if (activeSession != null && activeSession.isOpened()) { + accessTokenPortion = activeSession.getAccessToken(); + } + if (accessTokenPortion != null) { + // Cache-key collisions are not something to worry about here, since we only store state for + // one session. Even in the case where the previous session's serialized files have not been deleted yet, + // the objectSuffix will be different due to the session-change, thus making the key different. + accessTokenPortion = Utility.md5hash(accessTokenPortion); + } + return String.format( + "%s|%s|com.fb.sdk.like|%d", + objectId, + Utility.coerceValueIfNullOrEmpty(accessTokenPortion, ""), + objectSuffix); + } + + // + // Broadcast handling code + // + + private static void broadcastAction(Context context, LikeActionController controller, String action) { + broadcastAction(context, controller, action, null); + } + + private static void broadcastAction(Context context, LikeActionController controller, String action, Bundle data) { + Intent broadcastIntent = new Intent(action); + if (controller != null) { + if (data == null) { + data = new Bundle(); + } + + data.putString(ACTION_OBJECT_ID_KEY, controller.getObjectId()); + } + + if (data != null) { + broadcastIntent.putExtras(data); + } + LocalBroadcastManager.getInstance(context.getApplicationContext()).sendBroadcast(broadcastIntent); + } + + /** + * Constructor + */ + private LikeActionController(Context context, Session session, String objectId) { + this.context = context; + this.session = session; + this.objectId = objectId; + + appEventsLogger = AppEventsLogger.newLogger(context, session); + } + + /** + * Gets the the associated object id + * @return object id + */ + public String getObjectId() { + return objectId; + } + + /** + * Gets the String representation of the like-count for the associated object + * @return String representation of the like-count for the associated object + */ + public String getLikeCountString() { + return isObjectLiked ? likeCountStringWithLike : likeCountStringWithoutLike; + } + + /** + * Gets the String representation of the like-count for the associated object + * @return String representation of the like-count for the associated object + */ + public String getSocialSentence() { + return isObjectLiked ? socialSentenceWithLike : socialSentenceWithoutLike; + } + + /** + * Indicates whether the associated object is liked + * @return Indication of whether the associated object is liked + */ + public boolean isObjectLiked() { + return isObjectLiked; + } + + /** + * Entry-point to the code that performs the like/unlike action. + */ + public void toggleLike(Activity activity, Bundle analyticsParameters) { + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_TAP, null, analyticsParameters); + + boolean shouldLikeObject = !this.isObjectLiked; + if (canUseOGPublish(shouldLikeObject)) { + // Update UI state optimistically + updateState(shouldLikeObject, + this.likeCountStringWithLike, + this.likeCountStringWithoutLike, + this.socialSentenceWithLike, + this.socialSentenceWithoutLike, + this.unlikeToken); + if (isPendingLikeOrUnlike) { + // If the user toggled the button quickly, and there is still a publish underway, don't fire off + // another request. Also log this behavior. + + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_UNDO_QUICKLY, null, analyticsParameters); + return; + } + } + + performLikeOrUnlike(activity, shouldLikeObject, analyticsParameters); + } + + private void performLikeOrUnlike(Activity activity, boolean shouldLikeObject, Bundle analyticsParameters) { + if (canUseOGPublish(shouldLikeObject)) { + if (shouldLikeObject) { + publishLikeAsync(activity, analyticsParameters); + } else { + publishUnlikeAsync(activity, analyticsParameters); + } + } else { + presentLikeDialog(activity, analyticsParameters); + } + } + + private void updateState(boolean isObjectLiked, + String likeCountStringWithLike, + String likeCountStringWithoutLike, + String socialSentenceWithLike, + String socialSentenceWithoutLike, + String unlikeToken) { + // Normalize all empty strings to null, so that we don't have any problems with comparison. + likeCountStringWithLike = Utility.coerceValueIfNullOrEmpty(likeCountStringWithLike, null); + likeCountStringWithoutLike = Utility.coerceValueIfNullOrEmpty(likeCountStringWithoutLike, null); + socialSentenceWithLike = Utility.coerceValueIfNullOrEmpty(socialSentenceWithLike, null); + socialSentenceWithoutLike = Utility.coerceValueIfNullOrEmpty(socialSentenceWithoutLike, null); + unlikeToken = Utility.coerceValueIfNullOrEmpty(unlikeToken, null); + + boolean stateChanged = isObjectLiked != this.isObjectLiked || + !Utility.areObjectsEqual(likeCountStringWithLike, this.likeCountStringWithLike) || + !Utility.areObjectsEqual(likeCountStringWithoutLike, this.likeCountStringWithoutLike) || + !Utility.areObjectsEqual(socialSentenceWithLike, this.socialSentenceWithLike) || + !Utility.areObjectsEqual(socialSentenceWithoutLike, this.socialSentenceWithoutLike) || + !Utility.areObjectsEqual(unlikeToken, this.unlikeToken); + + if (!stateChanged) { + return; + } + + this.isObjectLiked = isObjectLiked; + this.likeCountStringWithLike = likeCountStringWithLike; + this.likeCountStringWithoutLike = likeCountStringWithoutLike; + this.socialSentenceWithLike = socialSentenceWithLike; + this.socialSentenceWithoutLike = socialSentenceWithoutLike; + this.unlikeToken = unlikeToken; + + serializeToDiskAsync(this); + + broadcastAction(context, this, ACTION_LIKE_ACTION_CONTROLLER_UPDATED); + } + + private void presentLikeDialog(Activity activity, Bundle analyticsParameters) { + LikeDialogBuilder likeDialogBuilder = new LikeDialogBuilder(activity, objectId); + + if (likeDialogBuilder.canPresent()) { + trackPendingCall(likeDialogBuilder.build().present(), analyticsParameters); + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_DIALOG, null, analyticsParameters); + } else { + String webFallbackUrl = likeDialogBuilder.getWebFallbackUrl(); + if (!Utility.isNullOrEmpty(webFallbackUrl)) { + boolean webFallbackShown = FacebookWebFallbackDialog.presentWebFallback( + activity, + webFallbackUrl, + likeDialogBuilder.getApplicationId(), + likeDialogBuilder.getAppCall(), + getFacebookDialogCallback(analyticsParameters)); + if (webFallbackShown) { + appEventsLogger.logSdkEvent( + AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_FALLBACK, null, analyticsParameters); + } + } + } + } + + private boolean onActivityResult(int requestCode, int resultCode, Intent data, UUID callId) { + if (pendingCallId == null || !pendingCallId.equals(callId)) { + return false; + } + + // See if we were waiting for a dialog completion + FacebookDialog.PendingCall pendingCall = PendingCallStore.getInstance().getPendingCallById(pendingCallId); + if (pendingCall == null) { + return false; + } + + // Look for results + FacebookDialog.handleActivityResult( + context, + pendingCall, + requestCode, + data, + getFacebookDialogCallback(pendingCallAnalyticsBundle)); + + // The handlers from above will run synchronously. So by the time we get here, it should be safe to + // stop tracking this call and also serialize the controller to disk + stopTrackingPendingCall(); + + return true; + } + + private FacebookDialog.Callback getFacebookDialogCallback(final Bundle analyticsParameters) { + return new FacebookDialog.Callback() { + @Override + public void onComplete(FacebookDialog.PendingCall pendingCall, Bundle data) { + if (!data.containsKey(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY)) { + // This is an empty result that we can't handle. Don't lose like state. + return; + } + + boolean isObjectLiked = data.getBoolean(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY); + String likeCountString = data.getString(LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY); + String socialSentence = data.getString(LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY); + String unlikeToken = data.getString(LIKE_DIALOG_RESPONSE_UNLIKE_TOKEN_KEY); + + Bundle logParams = (analyticsParameters == null) ? new Bundle() : analyticsParameters; + logParams.putString(AnalyticsEvents.PARAMETER_CALL_ID, pendingCall.getCallId().toString()); + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DIALOG_DID_SUCCEED, null, logParams); + + updateState( + isObjectLiked, + likeCountString, + likeCountString, + socialSentence, + socialSentence, + unlikeToken); + } + + @Override + public void onError(FacebookDialog.PendingCall pendingCall, Exception error, Bundle data) { + Logger.log(LoggingBehavior.REQUESTS, TAG, "Like Dialog failed with error : %s", error); + + Bundle logParams = analyticsParameters == null ? new Bundle() : analyticsParameters; + logParams.putString(AnalyticsEvents.PARAMETER_CALL_ID, pendingCall.getCallId().toString()); + + // Log the error and AppEvent + logAppEventForError("present_dialog", logParams); + + broadcastAction(context, LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, data); + } + }; + } + + private void trackPendingCall(FacebookDialog.PendingCall pendingCall, Bundle analyticsParameters) { + PendingCallStore.getInstance().trackPendingCall(pendingCall); + + // Save off the call id for processing the response + pendingCallId = pendingCall.getCallId(); + storeObjectIdForPendingController(objectId); + + // Store off the analytics parameters as well, for completion-logging + pendingCallAnalyticsBundle = analyticsParameters; + + // Serialize to disk, in case we get terminated while waiting for the dialog to complete + serializeToDiskAsync(this); + } + + private void stopTrackingPendingCall() { + PendingCallStore.getInstance().stopTrackingPendingCall(pendingCallId); + + pendingCallId = null; + pendingCallAnalyticsBundle = null; + + storeObjectIdForPendingController(null); + } + + private void storeObjectIdForPendingController(String objectId) { + objectIdForPendingController = objectId; + context.getSharedPreferences(LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE) + .edit() + .putString(LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY, objectIdForPendingController) + .apply(); + } + + private boolean canUseOGPublish(boolean willPerformLike) { + // Verify that the object isn't a Page, that we have permissions and that, if we're unliking, then + // we have an unlike token. + return !objectIsPage && + verifiedObjectId != null && + session != null && + session.getPermissions() != null && + session.getPermissions().contains("publish_actions") && + (willPerformLike || !Utility.isNullOrEmpty(unlikeToken)); + } + + private void publishLikeAsync(final Activity activity, final Bundle analyticsParameters) { + isPendingLikeOrUnlike = true; + + fetchVerifiedObjectId(new RequestCompletionCallback() { + @Override + public void onComplete() { + if (Utility.isNullOrEmpty(verifiedObjectId)) { + // Could not get a verified id + Bundle errorBundle = new Bundle(); + errorBundle.putString(NativeProtocol.STATUS_ERROR_DESCRIPTION, ERROR_INVALID_OBJECT_ID); + + broadcastAction(context, LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, errorBundle); + return; + } + + // Perform the Like. + RequestBatch requestBatch = new RequestBatch(); + final PublishLikeRequestWrapper likeRequest = new PublishLikeRequestWrapper(verifiedObjectId); + likeRequest.addToBatch(requestBatch); + requestBatch.addCallback(new RequestBatch.Callback() { + @Override + public void onBatchCompleted(RequestBatch batch) { + isPendingLikeOrUnlike = false; + + if (likeRequest.error != null) { + // We already updated the UI to show button in the Liked state. Since this failed, let's + // revert back to the Unliked state and show the dialog. We need to do this because the + // dialog-flow expects the button to only be updated once the dialog returns + + updateState( + false, + likeCountStringWithLike, + likeCountStringWithoutLike, + socialSentenceWithLike, + socialSentenceWithoutLike, + unlikeToken); + + presentLikeDialog(activity, analyticsParameters); + } else { + unlikeToken = Utility.coerceValueIfNullOrEmpty(likeRequest.unlikeToken, null); + isObjectLikedOnServer = true; + + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_LIKE, null, analyticsParameters); + + toggleAgainIfNeeded(activity, analyticsParameters); + } + } + }); + + requestBatch.executeAsync(); + } + }); + } + + private void publishUnlikeAsync(final Activity activity, final Bundle analyticsParameters) { + isPendingLikeOrUnlike = true; + + // Perform the Unlike. + RequestBatch requestBatch = new RequestBatch(); + final PublishUnlikeRequestWrapper unlikeRequest = new PublishUnlikeRequestWrapper(unlikeToken); + unlikeRequest.addToBatch(requestBatch); + requestBatch.addCallback(new RequestBatch.Callback() { + @Override + public void onBatchCompleted(RequestBatch batch) { + isPendingLikeOrUnlike = false; + + if (unlikeRequest.error != null) { + // We already updated the UI to show button in the Unliked state. Since this failed, let's + // revert back to the Liked state and show the dialog. We need to do this because the + // dialog-flow expects the button to only be updated once the dialog returns + + updateState( + true, + likeCountStringWithLike, + likeCountStringWithoutLike, + socialSentenceWithLike, + socialSentenceWithoutLike, + unlikeToken); + + presentLikeDialog(activity, analyticsParameters); + } else { + unlikeToken = null; + isObjectLikedOnServer = false; + + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_UNLIKE, null, analyticsParameters); + + toggleAgainIfNeeded(activity, analyticsParameters); + } + } + }); + + requestBatch.executeAsync(); + } + + private void refreshStatusAsync() { + if (session == null || session.isClosed() || SessionState.CREATED.equals(session.getState())) { + // Only when we know that there is no active session, or if there is, it is not open OR being opened, + // should we attempt getting like state from the service. Otherwise, use the access token of the session + // to make sure we get the correct like state. + refreshStatusViaService(); + return; + } + + fetchVerifiedObjectId(new RequestCompletionCallback() { + @Override + public void onComplete() { + final GetOGObjectLikesRequestWrapper objectLikesRequest = + new GetOGObjectLikesRequestWrapper(verifiedObjectId); + final GetEngagementRequestWrapper engagementRequest = + new GetEngagementRequestWrapper(verifiedObjectId); + + RequestBatch requestBatch = new RequestBatch(); + objectLikesRequest.addToBatch(requestBatch); + engagementRequest.addToBatch(requestBatch); + + requestBatch.addCallback(new RequestBatch.Callback() { + @Override + public void onBatchCompleted(RequestBatch batch) { + if (objectLikesRequest.error != null || + engagementRequest.error != null) { + // Refreshing is best-effort. If the refresh fails, don't lose old state. + Logger.log( + LoggingBehavior.REQUESTS, + TAG, + "Unable to refresh like state for id: '%s'", objectId); + return; + } + + updateState( + objectLikesRequest.objectIsLiked, + engagementRequest.likeCountStringWithLike, + engagementRequest.likeCountStringWithoutLike, + engagementRequest.socialSentenceStringWithLike, + engagementRequest.socialSentenceStringWithoutLike, + objectLikesRequest.unlikeToken); + } + }); + + requestBatch.executeAsync(); + } + }); + } + + private void refreshStatusViaService() { + LikeStatusClient likeStatusClient = new LikeStatusClient( + context, + Settings.getApplicationId(), + objectId); + if (!likeStatusClient.start()) { + return; + } + + LikeStatusClient.CompletedListener callback = new LikeStatusClient.CompletedListener() { + @Override + public void completed(Bundle result) { + if (result == null || !result.containsKey(NativeProtocol.EXTRA_OBJECT_IS_LIKED)) { + // Don't lose old state if the service response is incomplete. + return; + } + + boolean objectIsLiked = result.getBoolean(NativeProtocol.EXTRA_OBJECT_IS_LIKED); + String likeCountWithLike = result.getString(NativeProtocol.EXTRA_LIKE_COUNT_STRING_WITH_LIKE); + String likeCountWithoutLike = result.getString(NativeProtocol.EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE); + String socialSentenceWithLike = result.getString(NativeProtocol.EXTRA_SOCIAL_SENTENCE_WITH_LIKE); + String socialSentenceWithoutLike = result.getString(NativeProtocol.EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE); + String unlikeToken = result.getString(NativeProtocol.EXTRA_UNLIKE_TOKEN); + + updateState( + objectIsLiked, + likeCountWithLike, + likeCountWithoutLike, + socialSentenceWithLike, + socialSentenceWithoutLike, + unlikeToken); + } + }; + + likeStatusClient.setCompletedListener(callback); + } + + private void toggleAgainIfNeeded(Activity activity, Bundle analyticsParameters) { + if (isObjectLiked != isObjectLikedOnServer) { + performLikeOrUnlike(activity, isObjectLiked, analyticsParameters); + } + } + + private void fetchVerifiedObjectId(final RequestCompletionCallback completionHandler) { + if (!Utility.isNullOrEmpty(verifiedObjectId)) { + if (completionHandler != null) { + completionHandler.onComplete(); + } + + return; + } + + final GetOGObjectIdRequestWrapper objectIdRequest = new GetOGObjectIdRequestWrapper(objectId); + final GetPageIdRequestWrapper pageIdRequest = new GetPageIdRequestWrapper(objectId); + + RequestBatch requestBatch = new RequestBatch(); + objectIdRequest.addToBatch(requestBatch); + pageIdRequest.addToBatch(requestBatch); + + requestBatch.addCallback(new RequestBatch.Callback() { + @Override + public void onBatchCompleted(RequestBatch batch) { + verifiedObjectId = objectIdRequest.verifiedObjectId; + if (Utility.isNullOrEmpty(verifiedObjectId)) { + verifiedObjectId = pageIdRequest.verifiedObjectId; + objectIsPage = pageIdRequest.objectIsPage; + } + + if (Utility.isNullOrEmpty(verifiedObjectId)) { + Logger.log(LoggingBehavior.DEVELOPER_ERRORS, + TAG, + "Unable to verify the FB id for '%s'. Verify that it is a valid FB object or page", objectId); + logAppEventForError("get_verified_id", + pageIdRequest.error != null ? pageIdRequest.error : objectIdRequest.error); + } + + if (completionHandler != null) { + completionHandler.onComplete(); + } + } + }); + + requestBatch.executeAsync(); + } + + private void logAppEventForError(String action, Bundle parameters) { + Bundle logParams = new Bundle(parameters); + logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_ID, objectId); + logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_CURRENT_ACTION, action); + + appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_ERROR, null, logParams); + } + + private void logAppEventForError(String action, FacebookRequestError error) { + Bundle logParams = new Bundle(); + if (error != null) { + JSONObject requestResult = error.getRequestResult(); + if (requestResult != null) { + logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_ERROR_JSON, requestResult.toString()); + } + } + logAppEventForError(action, logParams); + } + + // + // Interfaces + // + + /** + * Used by the call to getControllerForObjectId() + */ + public interface CreationCallback { + public void onComplete(LikeActionController likeActionController); + } + + /** + * Used by all the request wrappers + */ + private interface RequestCompletionCallback { + void onComplete(); + } + + // + // Inner classes + // + + private class GetOGObjectIdRequestWrapper extends AbstractRequestWrapper { + String verifiedObjectId; + + GetOGObjectIdRequestWrapper(String objectId) { + super(objectId); + + Bundle objectIdRequestParams = new Bundle(); + objectIdRequestParams.putString("fields", "og_object.fields(id)"); + objectIdRequestParams.putString("ids", objectId); + + setRequest(new Request(session, "", objectIdRequestParams, HttpMethod.GET)); + } + + @Override + protected void processError(FacebookRequestError error) { + // If this object Id is for a Page, an error will be received for this request + // We will then rely on the other request to come through. + if (error.getErrorMessage().contains("og_object")) { + this.error = null; + } else { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error getting the FB id for object '%s' : %s", objectId, error); + } + } + + @Override + protected void processSuccess(Response response) { + JSONObject results = Utility.tryGetJSONObjectFromResponse(response.getGraphObject(), objectId); + if (results != null) { + // See if we can get the OG object Id out + JSONObject ogObject = results.optJSONObject("og_object"); + if (ogObject != null) { + verifiedObjectId = ogObject.optString("id"); + } + } + } + } + + private class GetPageIdRequestWrapper extends AbstractRequestWrapper { + String verifiedObjectId; + boolean objectIsPage; + + GetPageIdRequestWrapper(String objectId) { + super(objectId); + + Bundle pageIdRequestParams = new Bundle(); + pageIdRequestParams.putString("fields", "id"); + pageIdRequestParams.putString("ids", objectId); + + setRequest(new Request(session, "", pageIdRequestParams, HttpMethod.GET)); + } + + @Override + protected void processSuccess(Response response) { + JSONObject results = Utility.tryGetJSONObjectFromResponse(response.getGraphObject(), objectId); + if (results != null) { + verifiedObjectId = results.optString("id"); + objectIsPage = !Utility.isNullOrEmpty(verifiedObjectId); + } + } + + @Override + protected void processError(FacebookRequestError error) { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error getting the FB id for object '%s' : %s", objectId, error); + } + } + + private class PublishLikeRequestWrapper extends AbstractRequestWrapper { + String unlikeToken; + + PublishLikeRequestWrapper(String objectId) { + super(objectId); + + Bundle likeRequestParams = new Bundle(); + likeRequestParams.putString("object", objectId); + + setRequest(new Request(session, "me/og.likes", likeRequestParams, HttpMethod.POST)); + } + + @Override + protected void processSuccess(Response response) { + unlikeToken = Utility.safeGetStringFromResponse(response.getGraphObject(), "id"); + } + + @Override + protected void processError(FacebookRequestError error) { + int errorCode = error.getErrorCode(); + if (errorCode == ERROR_CODE_OBJECT_ALREADY_LIKED) { + // This isn't an error for us. Client was just out of sync with server + // This will prevent us from showing the dialog for this. + + // However, there is no unliketoken. So a subsequent unlike WILL show the dialog + this.error = null; + } else { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error liking object '%s' : %s", objectId, error); + logAppEventForError("publish_like", error); + } + } + } + + private class PublishUnlikeRequestWrapper extends AbstractRequestWrapper { + private String unlikeToken; + + PublishUnlikeRequestWrapper(String unlikeToken) { + super(null); + + this.unlikeToken = unlikeToken; + + setRequest(new Request(session, unlikeToken, null, HttpMethod.DELETE)); + } + + @Override + protected void processSuccess(Response response) { + } + + @Override + protected void processError(FacebookRequestError error) { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error unliking object with unlike token '%s' : %s", unlikeToken, error); + logAppEventForError("publish_unlike", error); + } + } + + private class GetOGObjectLikesRequestWrapper extends AbstractRequestWrapper { + boolean objectIsLiked; + String unlikeToken; + + GetOGObjectLikesRequestWrapper(String objectId) { + super(objectId); + + Bundle requestParams = new Bundle(); + requestParams.putString("fields", "id,application"); + requestParams.putString("object", objectId); + + setRequest(new Request(session, "me/og.likes", requestParams, HttpMethod.GET)); + } + + @Override + protected void processSuccess(Response response) { + JSONArray dataSet = Utility.tryGetJSONArrayFromResponse(response.getGraphObject(), "data"); + if (dataSet != null) { + for (int i = 0; i < dataSet.length(); i++) { + JSONObject data = dataSet.optJSONObject(i); + if (data != null) { + objectIsLiked = true; + JSONObject appData = data.optJSONObject("application"); + if (appData != null) { + if (Utility.areObjectsEqual(session.getApplicationId(), appData.optString("id"))) { + unlikeToken = data.optString("id"); + } + } + } + } + } + } + + @Override + protected void processError(FacebookRequestError error) { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error fetching like status for object '%s' : %s", objectId, error); + logAppEventForError("get_og_object_like", error); + } + } + + private class GetEngagementRequestWrapper extends AbstractRequestWrapper { + String likeCountStringWithLike; + String likeCountStringWithoutLike; + String socialSentenceStringWithLike; + String socialSentenceStringWithoutLike; + + GetEngagementRequestWrapper(String objectId) { + super(objectId); + + Bundle requestParams = new Bundle(); + requestParams.putString( + "fields", + "engagement.fields(" + + "count_string_with_like," + + "count_string_without_like," + + "social_sentence_with_like," + + "social_sentence_without_like)"); + + setRequest(new Request(session, objectId, requestParams, HttpMethod.GET)); + } + + @Override + protected void processSuccess(Response response) { + JSONObject engagementResults = Utility.tryGetJSONObjectFromResponse(response.getGraphObject(), "engagement"); + if (engagementResults != null) { + likeCountStringWithLike = engagementResults.optString("count_string_with_like"); + likeCountStringWithoutLike = engagementResults.optString("count_string_without_like"); + socialSentenceStringWithLike = engagementResults.optString("social_sentence_with_like"); + socialSentenceStringWithoutLike = engagementResults.optString("social_sentence_without_like"); + } + } + + @Override + protected void processError(FacebookRequestError error) { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error fetching engagement for object '%s' : %s", objectId, error); + logAppEventForError("get_engagement", error); + } + } + + private abstract class AbstractRequestWrapper { + private Request request; + protected String objectId; + + FacebookRequestError error; + + protected AbstractRequestWrapper(String objectId) { + this.objectId = objectId; + } + + void addToBatch(RequestBatch batch) { + batch.add(request); + } + + protected void setRequest(Request request) { + this.request = request; + // Make sure that our requests are hitting the latest version of the API known to this sdk. + request.setVersion(ServerProtocol.GRAPH_API_VERSION); + request.setCallback(new Request.Callback() { + @Override + public void onCompleted(Response response) { + error = response.getError(); + if (error != null) { + processError(error); + } else { + processSuccess(response); + } + } + }); + } + + protected void processError(FacebookRequestError error) { + Logger.log(LoggingBehavior.REQUESTS, + TAG, + "Error running request for object '%s' : %s", objectId, error); + } + + protected abstract void processSuccess(Response response); + } + + private enum LikeDialogFeature implements FacebookDialog.DialogFeature { + + LIKE_DIALOG(NativeProtocol.PROTOCOL_VERSION_20140701); + + private int minVersion; + + private LikeDialogFeature(int minVersion) { + this.minVersion = minVersion; + } + + public String getAction() { + return NativeProtocol.ACTION_LIKE_DIALOG; + } + + public int getMinVersion() { + return minVersion; + } + } + + private static class LikeDialogBuilder extends FacebookDialog.Builder { + private String objectId; + + public LikeDialogBuilder(Activity activity, String objectId) { + super(activity); + + this.objectId = objectId; + } + + @Override + protected EnumSet getDialogFeatures() { + return EnumSet.of(LikeDialogFeature.LIKE_DIALOG); + } + + @Override + protected Bundle getMethodArguments() { + Bundle methodArgs = new Bundle(); + + methodArgs.putString(NativeProtocol.METHOD_ARGS_OBJECT_ID, objectId); + + return methodArgs; + } + + public FacebookDialog.PendingCall getAppCall() { + return appCall; + } + + public String getApplicationId() { + return applicationId; + } + + public String getWebFallbackUrl() { + return getWebFallbackUrlInternal(); + } + } + + // Performs cache re-ordering/trimming to keep most-recently-used items up front + // ** NOTE ** It is expected that only _ONE_ MRUCacheWorkItem is ever running. This is enforced by + // setting the concurrency of the WorkQueue to 1. Changing the concurrency will most likely lead to errors. + private static class MRUCacheWorkItem implements Runnable { + private static ArrayList mruCachedItems = new ArrayList(); + private String cacheItem; + private boolean shouldTrim; + + MRUCacheWorkItem(String cacheItem, boolean shouldTrim) { + this.cacheItem = cacheItem; + this.shouldTrim = shouldTrim; + } + + @Override + public void run() { + if (cacheItem != null) { + mruCachedItems.remove(cacheItem); + mruCachedItems.add(0, cacheItem); + } + if (shouldTrim && mruCachedItems.size() >= MAX_CACHE_SIZE) { + int targetSize = MAX_CACHE_SIZE / 2; // Optimize for fewer trim-passes. + while (targetSize < mruCachedItems.size()) { + String cacheKey = mruCachedItems.remove(mruCachedItems.size() - 1); + + // Here is where we actually remove from the cache of LikeActionControllers. + cache.remove(cacheKey); + } + } + } + } + + private static class SerializeToDiskWorkItem implements Runnable { + private String cacheKey; + private String controllerJson; + + SerializeToDiskWorkItem(String cacheKey, String controllerJson) { + this.cacheKey = cacheKey; + this.controllerJson = controllerJson; + } + + @Override + public void run() { + serializeToDiskSynchronously(cacheKey, controllerJson); + } + } + + private static class CreateLikeActionControllerWorkItem implements Runnable { + private Context context; + private String objectId; + private CreationCallback callback; + + CreateLikeActionControllerWorkItem(Context context, String objectId, CreationCallback callback) { + this.context = context; + this.objectId = objectId; + this.callback = callback; + } + + @Override + public void run() { + createControllerForObjectId(context, objectId, callback); + } + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/LikeBoxCountView.java b/platforms/android/FacebookLib/src/com/facebook/internal/LikeBoxCountView.java new file mode 100644 index 000000000..5d9d001a9 --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/LikeBoxCountView.java @@ -0,0 +1,223 @@ +/** + * 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.*; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; +import com.facebook.android.R; + +/** + * 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 LikeBoxCountView extends FrameLayout { + + public enum LikeBoxCountViewCaretPosition { + LEFT, + TOP, + RIGHT, + BOTTOM + } + + private TextView likeCountLabel; + private LikeBoxCountViewCaretPosition caretPosition = LikeBoxCountViewCaretPosition.LEFT; + + private float caretHeight; + private float caretWidth; + private float borderRadius; + private Paint borderPaint; + private int textPadding; + private int additionalTextPadding; + + /** + * Constructor + * + * @param context Context for this View + */ + public LikeBoxCountView(Context context) { + super(context); + initialize(context); + } + + /** + * Sets the text for this view + * @param text + */ + public void setText(String text) { + likeCountLabel.setText(text); + } + + /** + * Sets the caret's position. This will trigger a layout of the view. + * @param caretPosition + */ + public void setCaretPosition(LikeBoxCountViewCaretPosition caretPosition) { + this.caretPosition = caretPosition; + + // Since the presence of a caret will move that edge closer to the text, let's add + // some padding (equal to caretHeight) in that same direction + switch (caretPosition) { + case LEFT: + setAdditionalTextPadding(additionalTextPadding, 0, 0, 0); + break; + case TOP: + setAdditionalTextPadding(0, additionalTextPadding, 0, 0); + break; + case RIGHT: + setAdditionalTextPadding(0, 0, additionalTextPadding, 0); + break; + case BOTTOM: + setAdditionalTextPadding(0, 0, 0, additionalTextPadding); + break; + } + + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int top = getPaddingTop(), left = getPaddingLeft(); + int right = getWidth() - getPaddingRight(), bottom = getHeight() - getPaddingBottom(); + + switch (caretPosition) { + case BOTTOM: + bottom -= caretHeight; + break; + case LEFT: + left += caretHeight; + break; + case TOP: + top += caretHeight; + break; + case RIGHT: + right -= caretHeight; + break; + } + + drawBorder(canvas, left, top, right, bottom); + } + + private void initialize(Context context) { + setWillNotDraw(false); // Required for the onDraw() method to be called on a FrameLayout + caretHeight = getResources().getDimension(R.dimen.com_facebook_likeboxcountview_caret_height); + caretWidth = getResources().getDimension(R.dimen.com_facebook_likeboxcountview_caret_width); + borderRadius = getResources().getDimension(R.dimen.com_facebook_likeboxcountview_border_radius); + + borderPaint = new Paint(); + borderPaint.setColor(getResources().getColor(R.color.com_facebook_likeboxcountview_border_color)); + borderPaint.setStrokeWidth(getResources().getDimension(R.dimen.com_facebook_likeboxcountview_border_width)); + borderPaint.setStyle(Paint.Style.STROKE); + + initializeLikeCountLabel(context); + + addView(likeCountLabel); + + setCaretPosition(this.caretPosition); + } + + private void initializeLikeCountLabel(Context context) { + likeCountLabel = new TextView(context); + LayoutParams likeCountLabelLayoutParams = new LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + likeCountLabel.setLayoutParams(likeCountLabelLayoutParams); + likeCountLabel.setGravity(Gravity.CENTER); + likeCountLabel.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.com_facebook_likeboxcountview_text_size)); + likeCountLabel.setTextColor(getResources().getColor(R.color.com_facebook_likeboxcountview_text_color)); + textPadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeboxcountview_text_padding); + + // Calculate the additional text padding that will be applied in the direction of the caret. + additionalTextPadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeboxcountview_caret_height); + } + + private void setAdditionalTextPadding(int left, int top, int right, int bottom) { + likeCountLabel.setPadding( + textPadding + left, + textPadding + top, + textPadding + right, + textPadding + bottom); + } + + private void drawBorder(Canvas canvas, float left, float top, float right, float bottom) { + Path borderPath = new Path(); + + float ovalSize = 2.0f * borderRadius; + + // Top left corner + borderPath.addArc(new RectF(left, top, left + ovalSize, top + ovalSize), -180, 90); + + // Top caret + if (caretPosition == LikeBoxCountViewCaretPosition.TOP) { + borderPath.lineTo(left + (right - left - caretWidth) / 2, top); + borderPath.lineTo(left + (right - left) / 2, top - caretHeight); + borderPath.lineTo(left + (right - left + caretWidth) / 2, top); + } + + // Move to top right corner + borderPath.lineTo(right - borderRadius, top); + + // Top right corner + borderPath.addArc(new RectF(right - ovalSize, top, right, top + ovalSize), -90, 90); + + // Right caret + if (caretPosition == LikeBoxCountViewCaretPosition.RIGHT) { + borderPath.lineTo(right, top + (bottom - top - caretWidth) / 2); + borderPath.lineTo(right + caretHeight, top + (bottom - top) / 2); + borderPath.lineTo(right, top + (bottom - top + caretWidth) / 2); + } + + // Move to bottom right corner + borderPath.lineTo(right, bottom - borderRadius); + + // Bottom right corner + borderPath.addArc(new RectF(right - ovalSize, bottom - ovalSize, right, bottom), 0, 90); + + // Bottom caret + if (caretPosition == LikeBoxCountViewCaretPosition.BOTTOM) { + borderPath.lineTo(left + (right - left + caretWidth) / 2, bottom); + borderPath.lineTo(left + (right - left) / 2, bottom + caretHeight); + borderPath.lineTo(left + (right - left - caretWidth) / 2, bottom); + } + + // Move to bottom left corner + borderPath.lineTo(left + borderRadius, bottom); + + // Bottom left corner + borderPath.addArc(new RectF(left, bottom - ovalSize, left + ovalSize, bottom), 90, 90); + + // Left caret + if (caretPosition == LikeBoxCountViewCaretPosition.LEFT) { + borderPath.lineTo(left, top + (bottom - top + caretWidth) / 2); + borderPath.lineTo(left - caretHeight, top + (bottom - top) / 2); + borderPath.lineTo(left, top + (bottom - top - caretWidth) / 2); + } + + // Move back to the beginning + borderPath.lineTo(left, top + borderRadius); + + canvas.drawPath(borderPath, borderPaint); + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/LikeButton.java b/platforms/android/FacebookLib/src/com/facebook/internal/LikeButton.java new file mode 100644 index 000000000..99039f708 --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/LikeButton.java @@ -0,0 +1,87 @@ +/** + * 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.Typeface; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.Button; +import com.facebook.android.R; + +/** + * 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 LikeButton extends Button { + + private boolean isLiked; + + /** + * Create the LikeButton . + * + * @see android.view.View#View(android.content.Context) + */ + public LikeButton(Context context, boolean isLiked) { + super(context); + + this.isLiked = isLiked; + + initialize(); + } + + public void setLikeState(boolean isLiked) { + if (isLiked != this.isLiked) { + this.isLiked = isLiked; + updateForLikeStatus(); + } + } + + private void initialize() { + // 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_VERTICAL); + this.setTextColor(getResources().getColor(R.color.com_facebook_likebutton_text_color)); + this.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.com_facebook_likebutton_text_size)); + this.setTypeface(Typeface.DEFAULT_BOLD); + + this.setCompoundDrawablePadding( + getResources().getDimensionPixelSize(R.dimen.com_facebook_likebutton_compound_drawable_padding)); + this.setPadding( + getResources().getDimensionPixelSize(R.dimen.com_facebook_likebutton_padding_left), + getResources().getDimensionPixelSize(R.dimen.com_facebook_likebutton_padding_top), + getResources().getDimensionPixelSize(R.dimen.com_facebook_likebutton_padding_right), + getResources().getDimensionPixelSize(R.dimen.com_facebook_likebutton_padding_bottom)); + + updateForLikeStatus(); + } + + private void updateForLikeStatus() { + if (isLiked) { + this.setBackgroundResource(R.drawable.com_facebook_button_like_selected); + this.setCompoundDrawablesWithIntrinsicBounds(R.drawable.com_facebook_button_like_icon_selected, 0, 0, 0); + this.setText(getResources().getString(R.string.com_facebook_like_button_liked)); + } else { + this.setBackgroundResource(R.drawable.com_facebook_button_like); + this.setCompoundDrawablesWithIntrinsicBounds(R.drawable.com_facebook_button_like_icon, 0, 0, 0); + this.setText(getResources().getString(R.string.com_facebook_like_button_not_liked)); + } + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/LikeStatusClient.java b/platforms/android/FacebookLib/src/com/facebook/internal/LikeStatusClient.java new file mode 100644 index 000000000..98a4113e9 --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/LikeStatusClient.java @@ -0,0 +1,48 @@ +/** + * 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.os.Bundle; + +/** + * 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. + */ + +/** + * This class executes service calls to fetch like-state of objects from the Facebook Application, if available. + */ +final class LikeStatusClient extends PlatformServiceClient { + private String objectId; + + LikeStatusClient(Context context, String applicationId, String objectId) { + super(context, NativeProtocol.MESSAGE_GET_LIKE_STATUS_REQUEST, NativeProtocol.MESSAGE_GET_LIKE_STATUS_REPLY, + NativeProtocol.PROTOCOL_VERSION_20141001, applicationId); + + this.objectId = objectId; + } + + @Override + protected void populateRequestBundle(Bundle data) { + // Only thing we need to pass in is the object id. + data.putString(NativeProtocol.EXTRA_OBJECT_ID, objectId); + } +} + diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java b/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java index a2cf01912..aae17e894 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/NativeProtocol.java @@ -61,6 +61,7 @@ public final class NativeProtocol { public static final int PROTOCOL_VERSION_20140204 = 20140204; public static final int PROTOCOL_VERSION_20140324 = 20140324; public static final int PROTOCOL_VERSION_20140701 = 20140701; + public static final int PROTOCOL_VERSION_20141001 = 20141001; 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"; @@ -81,12 +82,14 @@ public final class NativeProtocol { public static final String BRIDGE_ARG_ERROR_BUNDLE = "error"; // 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; + 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; + public static final int MESSAGE_GET_LIKE_STATUS_REQUEST = 0x10006; + public static final int MESSAGE_GET_LIKE_STATUS_REPLY = 0x10007; // MESSAGE_ERROR_REPLY data keys: // See STATUS_* @@ -99,6 +102,18 @@ public final class NativeProtocol { // EXTRA_EXPIRES_SECONDS_SINCE_EPOCH // EXTRA_PERMISSIONS + // MESSAGE_GET_LIKE_STATUS_REQUEST data keys: + // EXTRA_APPLICATION_ID + // EXTRA_OBJECT_ID + + // MESSAGE_GET_LIKE_STATUS_REPLY data keys: + // EXTRA_OBJECT_IS_LIKED + // EXTRA_LIKE_COUNT_STRING_WITH_LIKE + // EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE + // EXTRA_SOCIAL_SENTENCE_WITH_LIKE + // EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE + // EXTRA_UNLIKE_TOKEN + // MESSAGE_GET_PROTOCOL_VERSIONS_REPLY data keys: static final String EXTRA_PROTOCOL_VERSIONS = "com.facebook.platform.extra.PROTOCOL_VERSIONS"; @@ -109,6 +124,8 @@ public final class NativeProtocol { "com.facebook.platform.action.request.OGACTIONPUBLISH_DIALOG"; public static final String ACTION_OGMESSAGEPUBLISH_DIALOG = "com.facebook.platform.action.request.OGMESSAGEPUBLISH_DIALOG"; + public static final String ACTION_LIKE_DIALOG = + "com.facebook.platform.action.request.LIKE_DIALOG"; // Values of EXTRA_PROTOCOL_ACTION values returned by PlatformActivity: public static final String ACTION_FEED_DIALOG_REPLY = @@ -119,6 +136,8 @@ public final class NativeProtocol { "com.facebook.platform.action.reply.OGACTIONPUBLISH_DIALOG"; public static final String ACTION_OGMESSAGEPUBLISH_DIALOG_REPLY = "com.facebook.platform.action.reply.OGMESSAGEPUBLISH_DIALOG"; + public static final String ACTION_LIKE_DIALOG_REPLY = + "com.facebook.platform.action.reply.LIKE_DIALOG"; // Extras supported for ACTION_LOGIN_DIALOG: public static final String EXTRA_PERMISSIONS = "com.facebook.platform.extra.PERMISSIONS"; @@ -164,6 +183,17 @@ public final class NativeProtocol { public static final String METHOD_ARGS_ACTION_TYPE = "ACTION_TYPE"; public static final String METHOD_ARGS_PREVIEW_PROPERTY_NAME = "PREVIEW_PROPERTY_NAME"; + // Extras supported for MESSAGE_GET_LIKE_STATUS_REQUEST: + public static final String EXTRA_OBJECT_ID = "com.facebook.platform.extra.OBJECT_ID"; + + // Extras supported in MESSAGE_GET_LIKE_STATUS_REPLY: + public static final String EXTRA_OBJECT_IS_LIKED = "com.facebook.platform.extra.OBJECT_IS_LIKED"; + public static final String EXTRA_LIKE_COUNT_STRING_WITH_LIKE = "com.facebook.platform.extra.LIKE_COUNT_STRING_WITH_LIKE"; + public static final String EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE = "com.facebook.platform.extra.LIKE_COUNT_STRING_WITHOUT_LIKE"; + public static final String EXTRA_SOCIAL_SENTENCE_WITH_LIKE = "com.facebook.platform.extra.SOCIAL_SENTENCE_WITH_LIKE"; + public static final String EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE = "com.facebook.platform.extra.SOCIAL_SENTENCE_WITHOUT_LIKE"; + public static final String EXTRA_UNLIKE_TOKEN = "com.facebook.platform.extra.UNLIKE_TOKEN"; + // 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 @@ -171,6 +201,9 @@ public final class NativeProtocol { // url key for images public static final String IMAGE_URL_KEY = "url"; + // Method args supported for ACTION_LIKE_DIALOG + public static final String METHOD_ARGS_OBJECT_ID = "object_id"; + // 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"; @@ -296,6 +329,7 @@ private static Map> buildActionToAppInfoMap() { // Add individual actions and the list they should try map.put(ACTION_OGACTIONPUBLISH_DIALOG, facebookAppInfoList); map.put(ACTION_FEED_DIALOG, facebookAppInfoList); + map.put(ACTION_LIKE_DIALOG, facebookAppInfoList); map.put(ACTION_MESSAGE_DIALOG, messengerAppInfoList); map.put(ACTION_OGMESSAGEPUBLISH_DIALOG, messengerAppInfoList); @@ -387,9 +421,14 @@ public static Intent createTokenRefreshIntent(Context context) { return null; } + public static final int getLatestKnownVersion() { + return KNOWN_PROTOCOL_VERSIONS.get(0); + } + // 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_20141001, PROTOCOL_VERSION_20140701, PROTOCOL_VERSION_20140324, PROTOCOL_VERSION_20140204, @@ -449,7 +488,7 @@ public static Intent createPlatformActivityIntent( bridgeArguments.putString(BRIDGE_ARG_APP_NAME_STRING, applicationName); intent.putExtra(EXTRA_PROTOCOL_BRIDGE_ARGS, bridgeArguments); - Bundle methodArguments = extras == null ? new Bundle() : extras; + Bundle methodArguments = (extras == null) ? new Bundle() : extras; intent.putExtra(EXTRA_PROTOCOL_METHOD_ARGS, methodArguments); } else { // This is the older flat intent @@ -479,6 +518,9 @@ public static int getProtocolVersionFromIntent(Intent intent) { } public static UUID getCallIdFromIntent(Intent intent) { + if (intent == null) { + return null; + } int version = getProtocolVersionFromIntent(intent); String callIdString = null; if (isVersionCompatibleWithBucketedIntent(version)) { @@ -528,27 +570,30 @@ public static boolean isErrorResult(Intent resultIntent) { } } - public static Exception getErrorFromResult(Intent resultIntent) { + public static Bundle getErrorDataFromResultIntent(Intent resultIntent) { if (!isErrorResult(resultIntent)) { return null; } Bundle bridgeArgs = getBridgeArgumentsFromIntent(resultIntent); if (bridgeArgs != null) { - Bundle errorBundle = bridgeArgs.getBundle(BRIDGE_ARG_ERROR_BUNDLE); - if (errorBundle != null) { - return getErrorFromResult(errorBundle); - } + return bridgeArgs.getBundle(BRIDGE_ARG_ERROR_BUNDLE); + + } - return getErrorFromResult(resultIntent.getExtras()); + return resultIntent.getExtras(); } - public static Exception getErrorFromResult(Bundle errorBundle) { + public static Exception getExceptionFromErrorData(Bundle errorData) { + if (errorData == null) { + return null; + } + // TODO This is not going to work for JS dialogs, where the keys are not STATUS_ERROR_TYPE etc. // TODO However, it should keep existing dialogs functional - String type = errorBundle.getString(STATUS_ERROR_TYPE); - String description = errorBundle.getString(STATUS_ERROR_DESCRIPTION); + String type = errorData.getString(STATUS_ERROR_TYPE); + String description = errorData.getString(STATUS_ERROR_DESCRIPTION); if (type != null && type.equalsIgnoreCase(ERROR_USER_CANCELED)) { return new FacebookOperationCanceledException(description); @@ -560,23 +605,23 @@ public static Exception getErrorFromResult(Bundle errorBundle) { public static int getLatestAvailableProtocolVersionForService(Context context, final int minimumVersion) { // Services are currently always against the Facebook App - return getLatestAvailableProtocolVersionForAppInfoList(context, facebookAppInfoList, minimumVersion); + return getLatestAvailableProtocolVersionForAppInfoList(context, facebookAppInfoList, new int[] {minimumVersion}); } - public static int getLatestAvailableProtocolVersionForAction(Context context, String action, final int minimumVersion) { + public static int getLatestAvailableProtocolVersionForAction(Context context, String action, int[] versionSpec) { List appInfoList = actionToAppInfoMap.get(action); - return getLatestAvailableProtocolVersionForAppInfoList(context, appInfoList, minimumVersion); + return getLatestAvailableProtocolVersionForAppInfoList(context, appInfoList, versionSpec); } private static int getLatestAvailableProtocolVersionForAppInfoList(Context context, List appInfoList, - final int minimumVersion) { + int[] versionSpec) { if (appInfoList == null) { return NO_PROTOCOL_AVAILABLE; } // Could potentially cache the NativeAppInfo to latestProtocolVersion for (NativeAppInfo appInfo : appInfoList) { - int protocolVersion = getLatestAvailableProtocolVersionForAppInfo(context, appInfo, minimumVersion); + int protocolVersion = getLatestAvailableProtocolVersionForAppInfo(context, appInfo, versionSpec); if (protocolVersion != NO_PROTOCOL_AVAILABLE) { return protocolVersion; } @@ -585,8 +630,19 @@ private static int getLatestAvailableProtocolVersionForAppInfoList(Context conte return NO_PROTOCOL_AVAILABLE; } - private static int getLatestAvailableProtocolVersionForAppInfo(Context context, NativeAppInfo appInfo, - final int minimumVersion) { + private static int getLatestAvailableProtocolVersionForAppInfo( + Context context, + NativeAppInfo appInfo, + int[] versionSpec) { + TreeSet fbAppVersions = getAllAvailableProtocolVersionsForAppInfo(context, appInfo); + return computeLatestAvailableVersionFromVersionSpec(fbAppVersions, getLatestKnownVersion(), versionSpec); + } + + private static TreeSet getAllAvailableProtocolVersionsForAppInfo( + Context context, + NativeAppInfo appInfo) { + TreeSet allAvailableVersions = new TreeSet(); + ContentResolver contentResolver = context.getContentResolver(); String [] projection = new String[]{ PLATFORM_PROVIDER_VERSION_COLUMN }; @@ -594,23 +650,10 @@ private static int getLatestAvailableProtocolVersionForAppInfo(Context context, 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; + if (c != null) { + while (c.moveToNext()) { + int version = c.getInt(c.getColumnIndex(PLATFORM_PROVIDER_VERSION_COLUMN)); + allAvailableVersions.add(version); } } } finally { @@ -619,6 +662,53 @@ private static int getLatestAvailableProtocolVersionForAppInfo(Context context, } } + return allAvailableVersions; + } + + /** + * This is public to allow for testing. Developers are discouraged from using this method, since it may change without + * notice. + */ + public static int computeLatestAvailableVersionFromVersionSpec( + TreeSet allAvailableFacebookAppVersions, + int latestSdkVersion, + int[] versionSpec) { + // Remember that these ranges are sorted in ascending order and can be unbounded. So we are starting + // from the end of the version-spec array and working backwards, to try get the newest possible version + int versionSpecIndex = versionSpec.length - 1; + Iterator fbAppVersionsIterator = allAvailableFacebookAppVersions.descendingIterator(); + int latestFacebookAppVersion = -1; + + while (fbAppVersionsIterator.hasNext()) { + int fbAppVersion = fbAppVersionsIterator.next(); + + // We're holding on to the greatest fb-app version available. + latestFacebookAppVersion = Math.max(latestFacebookAppVersion, fbAppVersion); + + // If there is a newer version in the versionSpec, throw it away, we don't have it + while (versionSpecIndex >= 0 && versionSpec[versionSpecIndex] > fbAppVersion) { + versionSpecIndex--; + } + + if (versionSpecIndex < 0) { + // There was no fb app version that fell into any range in the versionSpec - or - the + // versionSpec was empty, which means that this action is not supported. + return NO_PROTOCOL_AVAILABLE; + } + + // If we are here, we know we are within a range specified in the versionSpec. We should see if it is + // a disabled or enabled range. + + if (versionSpec[versionSpecIndex] == fbAppVersion) { + // if the versionSpecIndex is even, it is enabled; if odd, disabled + return ( + versionSpecIndex % 2 == 0 ? + Math.min(latestFacebookAppVersion, latestSdkVersion) : + NO_PROTOCOL_AVAILABLE + ); + } + } + return NO_PROTOCOL_AVAILABLE; } diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/PendingCallStore.java b/platforms/android/FacebookLib/src/com/facebook/internal/PendingCallStore.java new file mode 100644 index 000000000..dca839a5c --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/internal/PendingCallStore.java @@ -0,0 +1,85 @@ +package com.facebook.internal; + +import android.content.Context; +import android.os.Bundle; +import com.facebook.widget.FacebookDialog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 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 PendingCallStore { + private static final String CALL_ID_ARRAY_KEY = "com.facebook.internal.PendingCallStore.callIdArrayKey"; + private static final String CALL_KEY_PREFIX = "com.facebook.internal.PendingCallStore."; + + private static PendingCallStore mInstance; + + private Map pendingCallMap = new HashMap(); + + public static PendingCallStore getInstance() { + if (mInstance == null) { + createInstance(); + } + + return mInstance; + } + + private synchronized static void createInstance() { + if (mInstance == null) { + mInstance = new PendingCallStore(); + } + } + + public void trackPendingCall(FacebookDialog.PendingCall pendingCall) { + if (pendingCall != null) { + pendingCallMap.put(pendingCall.getCallId().toString(), pendingCall); + } + } + + public void stopTrackingPendingCall(UUID callId) { + if (callId != null) { + pendingCallMap.remove(callId.toString()); + } + } + + public FacebookDialog.PendingCall getPendingCallById(UUID callId) { + if (callId == null) { + return null; + } + return pendingCallMap.get(callId.toString()); + } + + public void saveInstanceState(Bundle outState) { + ArrayList callIds = new ArrayList(pendingCallMap.keySet()); + outState.putStringArrayList(CALL_ID_ARRAY_KEY, callIds); + + for(FacebookDialog.PendingCall pendingCall : pendingCallMap.values()) { + String stateKey = getSavedStateKeyForPendingCallId(pendingCall.getCallId().toString()); + outState.putParcelable(stateKey, pendingCall); + } + } + + public void restoreFromSavedInstanceState(Bundle savedInstanceState) { + ArrayList callIds = savedInstanceState.getStringArrayList(CALL_ID_ARRAY_KEY); + if (callIds != null) { + for (String callId : callIds) { + String stateKey = getSavedStateKeyForPendingCallId(callId); + FacebookDialog.PendingCall pendingCall = savedInstanceState.getParcelable(stateKey); + + if (pendingCall != null) { + pendingCallMap.put(pendingCall.getCallId().toString(), pendingCall); + } + } + } + } + + private String getSavedStateKeyForPendingCallId(String pendingCallId) { + return CALL_KEY_PREFIX + pendingCallId; + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/PlatformServiceClient.java b/platforms/android/FacebookLib/src/com/facebook/internal/PlatformServiceClient.java index 16d619c3b..df75fc5e5 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/PlatformServiceClient.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/PlatformServiceClient.java @@ -96,7 +96,11 @@ public void onServiceConnected(ComponentName name, IBinder service) { public void onServiceDisconnected(ComponentName name) { sender = null; - context.unbindService(this); + try { + context.unbindService(this); + } catch (IllegalArgumentException ex) { + // Do nothing, the connection was already unbound + } callback(null); } diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java b/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java index bc9beecfa..7583f015d 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/ServerProtocol.java @@ -16,9 +16,16 @@ package com.facebook.internal; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import com.facebook.LoggingBehavior; import com.facebook.Settings; +import org.json.JSONException; +import org.json.JSONObject; import java.util.Collection; +import java.util.EnumSet; /** * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of @@ -26,6 +33,8 @@ * any time. */ public final class ServerProtocol { + private static final String TAG = ServerProtocol.class.getName(); + 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"; @@ -44,10 +53,18 @@ public final class ServerProtocol { public static final String DIALOG_RESPONSE_TYPE_TOKEN = "token"; public static final String DIALOG_RETURN_SCOPES_TRUE = "true"; + public static final String FALLBACK_DIALOG_PARAM_APP_ID = "app_id"; + public static final String FALLBACK_DIALOG_PARAM_BRIDGE_ARGS = "bridge_args"; + public static final String FALLBACK_DIALOG_PARAM_KEY_HASH = "android_key_hash"; + public static final String FALLBACK_DIALOG_PARAM_METHOD_ARGS = "method_args"; + public static final String FALLBACK_DIALOG_PARAM_METHOD_RESULTS = "method_results"; + public static final String FALLBACK_DIALOG_PARAM_VERSION = "version"; + public static final String FALLBACK_DIALOG_DISPLAY_VALUE_TOUCH = "touch"; + // URL components private static final String GRAPH_VIDEO_URL_FORMAT = "https://graph-video.%s"; private static final String GRAPH_URL_FORMAT = "https://graph.%s"; - public static final String GRAPH_API_VERSION = "v2.1"; + public static final String GRAPH_API_VERSION = "v2.2"; private static final String LEGACY_API_VERSION = "v1.0"; @@ -74,4 +91,48 @@ public static final String getAPIVersion() { } return GRAPH_API_VERSION; } + + public static Bundle getQueryParamsForPlatformActivityIntentWebFallback( + Context context, + String callId, + int version, + String applicationName, + Bundle methodArgs) { + + String keyHash = Settings.getApplicationSignature(context); + if (Utility.isNullOrEmpty(keyHash)) { + return null; + } + + Bundle webParams = new Bundle(); + + webParams.putString(FALLBACK_DIALOG_PARAM_KEY_HASH, keyHash); + webParams.putString(FALLBACK_DIALOG_PARAM_APP_ID, Settings.getApplicationId()); + webParams.putInt(FALLBACK_DIALOG_PARAM_VERSION, version); + webParams.putString(DIALOG_PARAM_DISPLAY, FALLBACK_DIALOG_DISPLAY_VALUE_TOUCH); + + Bundle bridgeArguments = new Bundle(); + bridgeArguments.putString(NativeProtocol.BRIDGE_ARG_ACTION_ID_STRING, callId); + bridgeArguments.putString(NativeProtocol.BRIDGE_ARG_APP_NAME_STRING, applicationName); + + methodArgs = (methodArgs == null) ? new Bundle() : methodArgs; + + try { + JSONObject bridgeArgsJSON = BundleJSONConverter.convertToJSON(bridgeArguments); + JSONObject methodArgsJSON = BundleJSONConverter.convertToJSON(methodArgs); + + if (bridgeArgsJSON == null || methodArgsJSON == null) { + return null; + } + + webParams.putString(FALLBACK_DIALOG_PARAM_BRIDGE_ARGS, bridgeArgsJSON.toString()); + webParams.putString(FALLBACK_DIALOG_PARAM_METHOD_ARGS, methodArgsJSON.toString()); + } catch (JSONException je) { + webParams = null; + Logger.log(LoggingBehavior.DEVELOPER_ERRORS, Log.ERROR, TAG, + "Error creating Url -- " + je); + } + + return webParams; + } } diff --git a/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java b/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java index 19a8f4047..3735e0cce 100644 --- a/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java +++ b/platforms/android/FacebookLib/src/com/facebook/internal/Utility.java @@ -17,9 +17,11 @@ package com.facebook.internal; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Parcelable; import android.provider.Settings.Secure; @@ -30,7 +32,6 @@ import com.facebook.FacebookException; import com.facebook.Request; import com.facebook.Settings; -import com.facebook.android.BuildConfig; import com.facebook.model.GraphObject; import org.json.JSONArray; import org.json.JSONException; @@ -42,6 +43,7 @@ import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.URLConnection; +import java.net.URLDecoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; @@ -57,17 +59,27 @@ public final class Utility { 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_SETTINGS_PREFS_STORE = "com.facebook.internal.preferences.APP_SETTINGS"; + private static final String APP_SETTINGS_PREFS_KEY_FORMAT = "com.facebook.internal.APP_SETTINGS.%s"; + private static final String APP_SETTING_SUPPORTS_ATTRIBUTION = "supports_attribution"; + private static final String APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING = "supports_implicit_sdk_logging"; + private static final String APP_SETTING_NUX_CONTENT = "gdpv4_nux_content"; + private static final String APP_SETTING_NUX_ENABLED = "gdpv4_nux_enabled"; + private static final String APP_SETTING_DIALOG_CONFIGS = "android_dialog_configs"; private static final String EXTRA_APP_EVENTS_INFO_FORMAT_VERSION = "a1"; - - private static final String [] APP_SETTING_FIELDS = new String[] { - SUPPORTS_ATTRIBUTION, - SUPPORTS_IMPLICIT_SDK_LOGGING, - NUX_CONTENT, - NUX_ENABLED + private static final String DIALOG_CONFIG_DIALOG_NAME_FEATURE_NAME_SEPARATOR = "\\|"; + private static final String DIALOG_CONFIG_NAME_KEY = "name"; + private static final String DIALOG_CONFIG_VERSIONS_KEY = "versions"; + private static final String DIALOG_CONFIG_URL_KEY = "url"; + + private final static String UTF8 = "UTF-8"; + + private static final String[] APP_SETTING_FIELDS = new String[] { + APP_SETTING_SUPPORTS_ATTRIBUTION, + APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING, + APP_SETTING_NUX_CONTENT, + APP_SETTING_NUX_ENABLED, + APP_SETTING_DIALOG_CONFIGS }; private static final String APPLICATION_FIELDS = "fields"; @@ -77,20 +89,25 @@ public final class Utility { private static Map fetchedAppSettings = new ConcurrentHashMap(); - public static class FetchedAppSettings { + private static AsyncTask initialAppSettingsLoadTask; + + public static class FetchedAppSettings { private boolean supportsAttribution; private boolean supportsImplicitLogging; private String nuxContent; private boolean nuxEnabled; + private Map> dialogConfigMap; private FetchedAppSettings(boolean supportsAttribution, boolean supportsImplicitLogging, String nuxContent, - boolean nuxEnabled) { + boolean nuxEnabled, + Map> dialogConfigMap) { this.supportsAttribution = supportsAttribution; this.supportsImplicitLogging = supportsImplicitLogging; this.nuxContent = nuxContent; this.nuxEnabled = nuxEnabled; + this.dialogConfigMap = dialogConfigMap; } public boolean supportsAttribution() { @@ -108,6 +125,185 @@ public String getNuxContent() { public boolean getNuxEnabled() { return nuxEnabled; } + + public Map> getDialogConfigurations() { + return dialogConfigMap; + } + } + + public static class DialogFeatureConfig { + private static DialogFeatureConfig parseDialogConfig(JSONObject dialogConfigJSON) { + String dialogNameWithFeature = dialogConfigJSON.optString(DIALOG_CONFIG_NAME_KEY); + if (Utility.isNullOrEmpty(dialogNameWithFeature)) { + return null; + } + + String[] components = dialogNameWithFeature.split(DIALOG_CONFIG_DIALOG_NAME_FEATURE_NAME_SEPARATOR); + if (components.length != 2) { + // We expect the format to be dialogName|FeatureName, where both components are non-empty. + return null; + } + + String dialogName = components[0]; + String featureName = components[1]; + if (isNullOrEmpty(dialogName) || isNullOrEmpty(featureName)) { + return null; + } + + String urlString = dialogConfigJSON.optString(DIALOG_CONFIG_URL_KEY); + Uri fallbackUri = null; + if (!Utility.isNullOrEmpty(urlString)) { + fallbackUri = Uri.parse(urlString); + } + + JSONArray versionsJSON = dialogConfigJSON.optJSONArray(DIALOG_CONFIG_VERSIONS_KEY); + + int[] featureVersionSpec = parseVersionSpec(versionsJSON); + + return new DialogFeatureConfig(dialogName, featureName, fallbackUri, featureVersionSpec); + } + + private static int[] parseVersionSpec(JSONArray versionsJSON) { + // Null signifies no overrides to the min-version as specified by the SDK. + // An empty array would basically turn off the dialog (i.e no supported versions), so DON'T default to that. + int[] versionSpec = null; + if (versionsJSON != null) { + int numVersions = versionsJSON.length(); + versionSpec = new int[numVersions]; + for (int i = 0; i < numVersions; i++) { + // See if the version was stored directly as an Integer + int version = versionsJSON.optInt(i, NativeProtocol.NO_PROTOCOL_AVAILABLE); + if (version == NativeProtocol.NO_PROTOCOL_AVAILABLE) { + // If not, then see if it was stored as a string that can be parsed out. + // If even that fails, then we will leave it as NO_PROTOCOL_AVAILABLE + String versionString = versionsJSON.optString(i); + if (!isNullOrEmpty(versionString)) { + try { + version = Integer.parseInt(versionString); + } catch (NumberFormatException nfe) { + logd(LOG_TAG, nfe); + version = NativeProtocol.NO_PROTOCOL_AVAILABLE; + } + } + } + + versionSpec[i] = version; + } + } + + return versionSpec; + } + + private String dialogName; + private String featureName; + private Uri fallbackUrl; + private int[] featureVersionSpec; + + private DialogFeatureConfig(String dialogName, String featureName, Uri fallbackUrl, int[] featureVersionSpec) { + this.dialogName = dialogName; + this.featureName = featureName; + this.fallbackUrl = fallbackUrl; + this.featureVersionSpec = featureVersionSpec; + } + + public String getDialogName() { + return dialogName; + } + + public String getFeatureName() { + return featureName; + } + + public Uri getFallbackUrl() { + return fallbackUrl; + } + + public int[] getVersionSpec() { + return featureVersionSpec; + } + } + + /** + * Each array represents a set of closed or open Range, like so: + * [0,10,50,60] - Ranges are {0-9}, {50-59} + * [20] - Ranges are {20-} + * [30,40,100] - Ranges are {30-39}, {100-} + * + * All Ranges in the array have a closed lower bound. Only the last Range in each array may be open. + * It is assumed that the passed in arrays are sorted with ascending order. + * It is assumed that no two elements in a given are equal (i.e. no 0-length ranges) + * + * The method returns an intersect of the two passed in Range-sets + * @param range1 + * @param range2 + * @return + */ + public static int[] intersectRanges(int[] range1, int[] range2) { + if (range1 == null) { + return range2; + } else if (range2 == null) { + return range1; + } + + int[] outputRange = new int[range1.length + range2.length]; + int outputIndex = 0; + int index1 = 0, lower1, upper1; + int index2 = 0, lower2, upper2; + while (index1 < range1.length && index2 < range2.length) { + int newRangeLower = Integer.MIN_VALUE, newRangeUpper = Integer.MAX_VALUE; + lower1 = range1[index1]; + upper1 = Integer.MAX_VALUE; + + lower2 = range2[index2]; + upper2 = Integer.MAX_VALUE; + + if (index1 < range1.length - 1) { + upper1 = range1[index1 + 1]; + } + if (index2 < range2.length - 1) { + upper2 = range2[index2 + 1]; + } + + if (lower1 < lower2) { + if (upper1 > lower2) { + newRangeLower = lower2; + if (upper1 > upper2) { + newRangeUpper = upper2; + index2 += 2; + } else { + newRangeUpper = upper1; + index1 += 2; + } + } else { + index1 += 2; + } + } else { + if (upper2 > lower1) { + newRangeLower = lower1; + if (upper2 > upper1) { + newRangeUpper = upper1; + index1 += 2; + } else { + newRangeUpper = upper2; + index2 += 2; + } + } else { + index2 += 2; + } + } + + if (newRangeLower != Integer.MIN_VALUE) { + outputRange[outputIndex ++] = newRangeLower; + if (newRangeUpper != Integer.MAX_VALUE) { + outputRange[outputIndex ++] = newRangeUpper; + } else { + // If we reach an unbounded/open range, then we know we're done. + break; + } + } + } + + return Arrays.copyOf(outputRange, outputIndex); } // Returns true iff all items in subset are in superset, treating null and @@ -135,6 +331,23 @@ public static boolean isNullOrEmpty(String s) { return (s == null) || (s.length() == 0); } + /** + * Use this when you want to normalize empty and null strings + * This way, Utility.areObjectsEqual can used for comparison, where a null string is to be treated the same as + * an empty string. + * + * @param s + * @param valueIfNullOrEmpty + * @return + */ + public static String coerceValueIfNullOrEmpty(String s, String valueIfNullOrEmpty) { + if (isNullOrEmpty(s)) { + return valueIfNullOrEmpty; + } + + return s; + } + public static Collection unmodifiableCollection(T... ts) { return Collections.unmodifiableCollection(Arrays.asList(ts)); } @@ -198,6 +411,32 @@ public static Uri buildUri(String authority, String path, Bundle parameters) { return builder.build(); } + public static Bundle parseUrlQueryString(String queryString) { + Bundle params = new Bundle(); + if (!isNullOrEmpty(queryString)) { + String array[] = queryString.split("&"); + for (String parameter : array) { + String keyValuePair[] = parameter.split("="); + + try { + if (keyValuePair.length == 2) { + params.putString( + URLDecoder.decode(keyValuePair[0], UTF8), + URLDecoder.decode(keyValuePair[1], UTF8)); + } else if (keyValuePair.length == 1) { + params.putString( + URLDecoder.decode(keyValuePair[0], UTF8), + ""); + } + } catch (UnsupportedEncodingException e) { + // shouldn't happen + logd(LOG_TAG, e); + } + } + } + return params; + } + public static void putObjectInBundle(Bundle bundle, String key, Object value) { if (value instanceof String) { bundle.putString(key, (String) value); @@ -222,7 +461,7 @@ public static void closeQuietly(Closeable closeable) { public static void disconnectQuietly(URLConnection connection) { if (connection instanceof HttpURLConnection) { - ((HttpURLConnection)connection).disconnect(); + ((HttpURLConnection) connection).disconnect(); } } @@ -352,17 +591,23 @@ public static void clearFacebookCookies(Context context) { } public static void logd(String tag, Exception e) { - if (Settings.isLoggingEnabled() && tag != null && e != null) { + if (Settings.isDebugEnabled() && tag != null && e != null) { Log.d(tag, e.getClass().getSimpleName() + ": " + e.getMessage()); } } public static void logd(String tag, String msg) { - if (Settings.isLoggingEnabled() && tag != null && msg != null) { + if (Settings.isDebugEnabled() && tag != null && msg != null) { Log.d(tag, msg); } } + public static void logd(String tag, String msg, Throwable t) { + if (Settings.isDebugEnabled() && !isNullOrEmpty(tag)) { + Log.d(tag, msg, t); + } + } + public static boolean areObjectsEqual(T a, T b) { if (a == null) { return b == null; @@ -370,34 +615,142 @@ public static boolean areObjectsEqual(T a, T b) { return a.equals(b); } + public static void loadAppSettingsAsync(final Context context, final String applicationId) { + if (Utility.isNullOrEmpty(applicationId) || + fetchedAppSettings.containsKey(applicationId) || + initialAppSettingsLoadTask != null) { + return; + } + + final String settingsKey = String.format(APP_SETTINGS_PREFS_KEY_FORMAT, applicationId); + + initialAppSettingsLoadTask = new AsyncTask() { + @Override + protected GraphObject doInBackground(Void... params) { + return getAppSettingsQueryResponse(applicationId); + } + + @Override + protected void onPostExecute(GraphObject result) { + if (result != null) { + JSONObject resultJSON = result.getInnerJSONObject(); + parseAppSettingsFromJSON(applicationId, resultJSON); + + SharedPreferences sharedPrefs = context.getSharedPreferences( + APP_SETTINGS_PREFS_STORE, + Context.MODE_PRIVATE); + sharedPrefs.edit() + .putString(settingsKey, resultJSON.toString()) + .apply(); + } + + initialAppSettingsLoadTask = null; + } + }; + initialAppSettingsLoadTask.execute((Void[])null); + + // Also see if we had a cached copy and use that immediately. + SharedPreferences sharedPrefs = context.getSharedPreferences( + APP_SETTINGS_PREFS_STORE, + Context.MODE_PRIVATE); + String settingsJSONString = sharedPrefs.getString(settingsKey, null); + if (!isNullOrEmpty(settingsJSONString)) { + JSONObject settingsJSON = null; + try { + settingsJSON = new JSONObject(settingsJSONString); + } catch (JSONException je) { + logd(LOG_TAG, je); + } + if (settingsJSON != null) { + parseAppSettingsFromJSON(applicationId, settingsJSON); + } + } + } + // 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); } + GraphObject response = getAppSettingsQueryResponse(applicationId); + if (response == null) { + return null; + } + + return parseAppSettingsFromJSON(applicationId, response.getInnerJSONObject()); + } + + private static FetchedAppSettings parseAppSettingsFromJSON(String applicationId, JSONObject settingsJSON) { + FetchedAppSettings result = new FetchedAppSettings( + settingsJSON.optBoolean(APP_SETTING_SUPPORTS_ATTRIBUTION, false), + settingsJSON.optBoolean(APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING, false), + settingsJSON.optString(APP_SETTING_NUX_CONTENT, ""), + settingsJSON.optBoolean(APP_SETTING_NUX_ENABLED, false), + parseDialogConfigurations(settingsJSON.optJSONObject(APP_SETTING_DIALOG_CONFIGS)) + ); + + fetchedAppSettings.put(applicationId, result); + + return result; + } + + // Note that this method makes a synchronous Graph API call, so should not be called from the main thread. + private static GraphObject getAppSettingsQueryResponse(String applicationId) { Bundle appSettingsParams = new Bundle(); appSettingsParams.putString(APPLICATION_FIELDS, TextUtils.join(",", APP_SETTING_FIELDS)); Request request = Request.newGraphPathRequest(null, applicationId, null); + request.setSkipClientToken(true); 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) - ); + GraphObject response = request.executeAndWait().getGraphObject(); + return response; + } - fetchedAppSettings.put(applicationId, result); + public static DialogFeatureConfig getDialogFeatureConfig(String applicationId, String actionName, String featureName) { + if (Utility.isNullOrEmpty(actionName) || Utility.isNullOrEmpty(featureName)) { + return null; + } - return result; + FetchedAppSettings settings = fetchedAppSettings.get(applicationId); + if (settings != null) { + Map featureMap = settings.getDialogConfigurations().get(actionName); + if (featureMap != null) { + return featureMap.get(featureName); + } + } + return null; } - private static boolean safeGetBooleanFromResponse(GraphObject response, String propertyName) { + private static Map> parseDialogConfigurations(JSONObject dialogConfigResponse) { + HashMap> dialogConfigMap = new HashMap>(); + + if (dialogConfigResponse != null) { + JSONArray dialogConfigData = dialogConfigResponse.optJSONArray("data"); + if (dialogConfigData != null) { + for (int i = 0; i < dialogConfigData.length(); i++) { + DialogFeatureConfig dialogConfig = DialogFeatureConfig.parseDialogConfig(dialogConfigData.optJSONObject(i)); + if (dialogConfig == null) { + continue; + } + + String dialogName = dialogConfig.getDialogName(); + Map featureMap = dialogConfigMap.get(dialogName); + if (featureMap == null) { + featureMap = new HashMap(); + dialogConfigMap.put(dialogName, featureMap); + } + featureMap.put(dialogConfig.getFeatureName(), dialogConfig); + } + } + } + + return dialogConfigMap; + } + + public static boolean safeGetBooleanFromResponse(GraphObject response, String propertyName) { Object result = false; if (response != null) { result = response.getProperty(propertyName); @@ -408,7 +761,7 @@ private static boolean safeGetBooleanFromResponse(GraphObject response, String p return (Boolean) result; } - private static String safeGetStringFromResponse(GraphObject response, String propertyName) { + public static String safeGetStringFromResponse(GraphObject response, String propertyName) { Object result = ""; if (response != null) { result = response.getProperty(propertyName); @@ -419,6 +772,28 @@ private static String safeGetStringFromResponse(GraphObject response, String pro return (String) result; } + public static JSONObject tryGetJSONObjectFromResponse(GraphObject response, String propertyKey) { + if (response == null) { + return null; + } + Object property = response.getProperty(propertyKey); + if (!(property instanceof JSONObject)) { + return null; + } + return (JSONObject) property; + } + + public static JSONArray tryGetJSONArrayFromResponse(GraphObject response, String propertyKey) { + if (response == null) { + return null; + } + Object property = response.getProperty(propertyKey); + if (!(property instanceof JSONArray)) { + return null; + } + return (JSONArray) property; + } + public static void clearCaches(Context context) { ImageDownloader.clearCache(context); } @@ -461,7 +836,7 @@ public static String getHashedDeviceAndAppID(Context context, String application } public static void setAppEventAttributionParameters(GraphObject params, - AttributionIdentifiers attributionIdentifiers, String hashedDeviceAndAppId, boolean limitEventUsage) { + 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()); @@ -478,28 +853,28 @@ public static void setAppEventAttributionParameters(GraphObject params, } public static void setAppEventExtendedDeviceInfoParameters(GraphObject params, Context appContext) { - JSONArray extraInfoArray = new JSONArray(); - extraInfoArray.put(EXTRA_APP_EVENTS_INFO_FORMAT_VERSION); + JSONArray extraInfoArray = new JSONArray(); + extraInfoArray.put(EXTRA_APP_EVENTS_INFO_FORMAT_VERSION); - // Application Manifest info: - String pkgName = appContext.getPackageName(); - int versionCode = -1; - String versionName = ""; + // Application Manifest info: + String pkgName = appContext.getPackageName(); + int versionCode = -1; + String versionName = ""; - try { - PackageInfo pi = appContext.getPackageManager().getPackageInfo(pkgName, 0); - versionCode = pi.versionCode; - versionName = pi.versionName; - } catch (PackageManager.NameNotFoundException e) { - // Swallow - } + try { + PackageInfo pi = appContext.getPackageManager().getPackageInfo(pkgName, 0); + versionCode = pi.versionCode; + versionName = pi.versionName; + } catch (PackageManager.NameNotFoundException e) { + // Swallow + } - // Application Manifest info: - extraInfoArray.put(pkgName); - extraInfoArray.put(versionCode); - extraInfoArray.put(versionName); + // Application Manifest info: + extraInfoArray.put(pkgName); + extraInfoArray.put(versionCode); + extraInfoArray.put(versionName); - params.setProperty("extinfo", extraInfoArray.toString()); + params.setProperty("extinfo", extraInfoArray.toString()); } public static Method getMethodQuietly(Class clazz, String methodName, Class... parameterTypes) { @@ -529,16 +904,16 @@ public static Object invokeMethodQuietly(Object receiver, Method method, Object. } } - /** - * Returns the name of the current activity if the context is an activity, otherwise return "unknown" - */ - public static String getActivityName(Context context) { - if (context == null) { - return "null"; - } else if (context == context.getApplicationContext()) { - return "unknown"; - } else { - return context.getClass().getSimpleName(); + /** + * Returns the name of the current activity if the context is an activity, otherwise return "unknown" + */ + public static String getActivityName(Context context) { + if (context == null) { + return "null"; + } else if (context == context.getApplicationContext()) { + return "unknown"; + } else { + return context.getClass().getSimpleName(); + } } - } } diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java b/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java index 226e1206a..5e17e1b4c 100644 --- a/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java +++ b/platforms/android/FacebookLib/src/com/facebook/widget/FacebookDialog.java @@ -20,15 +20,13 @@ import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; +import android.net.Uri; 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.internal.*; import com.facebook.model.GraphObject; import com.facebook.model.GraphObjectList; import com.facebook.model.OpenGraphAction; @@ -80,9 +78,30 @@ public interface Callback { void onError(PendingCall pendingCall, Exception error, Bundle data); } - private interface DialogFeature { + /** + * Provides an interface for describing a specific feature provided by a FacebookDialog. + * This is public primarily to allow its use elsewhere in the Android SDK; developers are discouraged from + * constructing their own DialogFeature implementations internal API may change. + */ + public interface DialogFeature { + /** + * This method is for internal use only. + */ String getAction(); + + /** + * This method is for internal use only. + */ int getMinVersion(); + + /** + * This method is for internal use only. + * + * For all Enums that implement this interface, the name() method is already present. It returns the String + * representation of the Enum value, verbatim. + * + */ + String name(); } /** @@ -343,12 +362,13 @@ public static boolean handleActivityResult(Context context, PendingCall appCall, if (callback != null) { if (NativeProtocol.isErrorResult(data)) { - Exception error = NativeProtocol.getErrorFromResult(data); + Bundle errorData = NativeProtocol.getErrorDataFromResultIntent(data); + Exception error = NativeProtocol.getExceptionFromErrorData(errorData); - // TODO - data.getExtras() doesn't work for the bucketed protocol. - callback.onError(appCall, error, data.getExtras()); + callback.onError(appCall, error, errorData); } else { - callback.onComplete(appCall, NativeProtocol.getSuccessResultsFromIntent(data)); + Bundle successResults = NativeProtocol.getSuccessResultsFromIntent(data); + callback.onComplete(appCall, successResults); } } @@ -416,12 +436,20 @@ public static boolean canPresentOpenGraphMessageDialog(Context context, OpenGrap } private static boolean handleCanPresent(Context context, Iterable features) { - return getProtocolVersionForNativeDialog(context, getActionForFeatures(features), getMinVersionForFeatures(features)) - != NativeProtocol.NO_PROTOCOL_AVAILABLE; + String actionName = getActionForFeatures(features); + String applicationId = Settings.getApplicationId(); + if (Utility.isNullOrEmpty(applicationId)) { + applicationId = Utility.getMetadataApplicationId(context); + } + return getProtocolVersionForNativeDialog( + context, + actionName, + getVersionSpecForFeatures(applicationId, actionName, features) + ) != NativeProtocol.NO_PROTOCOL_AVAILABLE; } - private static int getProtocolVersionForNativeDialog(Context context, String action, int requiredVersion) { - return NativeProtocol.getLatestAvailableProtocolVersionForAction(context, action, requiredVersion); + private static int getProtocolVersionForNativeDialog(Context context, String action, int[] versionSpec) { + return NativeProtocol.getLatestAvailableProtocolVersionForAction(context, action, versionSpec); } private static NativeAppCallAttachmentStore getAttachmentStore() { @@ -431,13 +459,27 @@ private static NativeAppCallAttachmentStore getAttachmentStore() { return attachmentStore; } - private static int getMinVersionForFeatures(Iterable features) { - int minVersion = Integer.MIN_VALUE; + private static int[] getVersionSpecForFeatures( + String applicationId, + String actionName, + Iterable features) { + int[] intersectedRange = null; // Null is treated as a fully open Range. So it is safe to compare against. 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()); + int[] featureVersionSpec = getVersionSpecForFeature(applicationId, actionName, feature); + intersectedRange = Utility.intersectRanges(intersectedRange, featureVersionSpec); + } + + return intersectedRange; + } + + private static int[] getVersionSpecForFeature(String applicationId, String actionName, DialogFeature feature) { + // Return the value from DialogFeatureConfig if available. Otherwise, just default to the min-version + Utility.DialogFeatureConfig config = Utility.getDialogFeatureConfig(applicationId, actionName, feature.name()); + if (config != null) { + return config.getVersionSpec(); + } else { + return new int[]{feature.getMinVersion()}; } - return minVersion; } private static String getActionForFeatures(Iterable features) { @@ -479,6 +521,8 @@ static private String getEventName(String action, boolean hasPhotos) { 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 if (action.equals(NativeProtocol.ACTION_LIKE_DIALOG)) { + eventName = AnalyticsEvents.EVENT_NATIVE_DIALOG_TYPE_LIKE; } else { throw new FacebookException("An unspecified action was presented"); } @@ -568,7 +612,7 @@ public FacebookDialog build() { String action = getActionForFeatures(getDialogFeatures()); int protocolVersion = getProtocolVersionForNativeDialog(activity, action, - getMinVersionForFeatures(getDialogFeatures())); + getVersionSpecForFeatures(applicationId, action, getDialogFeatures())); Bundle extras = null; if (NativeProtocol.isVersionCompatibleWithBucketedIntent(protocolVersion)) { @@ -600,6 +644,54 @@ public FacebookDialog build() { return new FacebookDialog(activity, fragment, appCall, getOnPresentCallback()); } + /** + * This is public primarily to allow its use elsewhere in the Android SDK; developers are discouraged from + * consuming this method as the internal API may change. + */ + protected String getWebFallbackUrlInternal() { + Iterable features = getDialogFeatures(); + String featureName = null; + String action = null; + for (DialogFeature feature : features) { + // All actions in a set of DialogFeatures should have the same fallback url + // So we can break after assigning the first one + featureName = feature.name(); + action = feature.getAction(); + break; + } + + Utility.DialogFeatureConfig config = Utility.getDialogFeatureConfig(applicationId, action, featureName); + Uri fallbackUrl; + if (config == null || (fallbackUrl = config.getFallbackUrl()) == null) { + return null; + } + + // Since we're talking to the server here, let's use the latest version we know about. + // We know we are going to be communicating over a bucketed protocol. + Bundle methodArguments = getMethodArguments(); + int protocolVersion = NativeProtocol.getLatestKnownVersion(); + Bundle webParams = ServerProtocol.getQueryParamsForPlatformActivityIntentWebFallback( + activity, + appCall.getCallId().toString(), + protocolVersion, + applicationName, + methodArguments); + if (webParams == null) { + // Could not create the query parameters + return null; + } + + // Now form the Uri + if (fallbackUrl.isRelative()) { + fallbackUrl = Utility.buildUri( + ServerProtocol.getDialogAuthority(), + fallbackUrl.toString(), + webParams); + } + + return fallbackUrl.toString(); + } + /** * 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 @@ -664,7 +756,10 @@ List getImageAttachmentNames() { return new ArrayList(imageAttachments.keySet()); } - protected abstract Bundle setBundleExtras(Bundle extras); + protected Bundle setBundleExtras(Bundle extras) { + // Default implementation. + return extras; + } protected abstract Bundle getMethodArguments(); @@ -674,7 +769,7 @@ protected void putExtra(Bundle extras, String key, String value) { } } - abstract EnumSet getDialogFeatures(); + protected abstract EnumSet getDialogFeatures(); protected CONCRETE addImageAttachment(String imageName, Bitmap bitmap) { imageAttachments.put(imageName, bitmap); @@ -712,9 +807,9 @@ public ShareDialogBuilderBase(Activity activity) { } /** - * Sets the title of the item to be shared. + * Sets the name of the URL to be shared. This method only has effect if setLink is called. * - * @param name the title + * @param name the name * @return this instance of the builder */ public CONCRETE setName(String name) { @@ -725,9 +820,9 @@ public CONCRETE setName(String name) { } /** - * Sets the subtitle of the item to be shared. + * Sets the caption of the URL to be shared. This method only has effect if setLink is called. * - * @param caption the subtitle + * @param caption the caption * @return this instance of the builder */ public CONCRETE setCaption(String caption) { @@ -738,7 +833,7 @@ public CONCRETE setCaption(String caption) { } /** - * Sets the description of the item to be shared. + * Sets the description of the URL to be shared. This method only has effect if setLink is called. * * @param description the description * @return this instance of the builder @@ -764,7 +859,7 @@ public CONCRETE setLink(String link) { } /** - * Sets the URL of the image of the item to be shared. + * Sets the URL of the image of the URL to be shared. This method only has effect if setLink is called. * * @param picture the URL of the image * @return this instance of the builder @@ -889,7 +984,7 @@ public ShareDialogBuilder(Activity activity) { } @Override - EnumSet getDialogFeatures() { + protected EnumSet getDialogFeatures() { return EnumSet.of(ShareDialogFeature.SHARE_DIALOG); } } @@ -1032,7 +1127,7 @@ public PhotoShareDialogBuilder(Activity activity) { } @Override - EnumSet getDialogFeatures() { + protected EnumSet getDialogFeatures() { return EnumSet.of(ShareDialogFeature.SHARE_DIALOG, ShareDialogFeature.PHOTOS); } @@ -1059,7 +1154,7 @@ public PhotoMessageDialogBuilder(Activity activity) { } @Override - EnumSet getDialogFeatures() { + protected EnumSet getDialogFeatures() { return EnumSet.of(MessageDialogFeature.MESSAGE_DIALOG, MessageDialogFeature.PHOTOS); } @@ -1067,6 +1162,28 @@ EnumSet getDialogFeatures() { int getMaximumNumberOfPhotos() { return MAXIMUM_PHOTO_COUNT; } + + /** + * setPlace is not supported for the photo message dialog, setting this method will have no effect. + * + * @param place will be ignored + * @return this instance of the builder + */ + @Override + public PhotoMessageDialogBuilder setPlace(String place) { + return this; + } + + /** + * setFriends is not supported for the photo message dialog, setting this method will have no effect. + * + * @param friends will be ignored + * @return this instance of the builder + */ + @Override + public PhotoMessageDialogBuilder setFriends(List friends) { + return this; + } } /** @@ -1088,9 +1205,31 @@ public MessageDialogBuilder(Activity activity) { } @Override - EnumSet getDialogFeatures() { + protected EnumSet getDialogFeatures() { return EnumSet.of(MessageDialogFeature.MESSAGE_DIALOG); } + + /** + * setPlace is not supported for the message dialog, setting this method will have no effect. + * + * @param place will be ignored + * @return this instance of the builder + */ + @Override + public MessageDialogBuilder setPlace(String place) { + return this; + } + + /** + * setFriends is not supported for the message dialog, setting this method will have no effect. + * + * @param friends will be ignored + * @return this instance of the builder + */ + @Override + public MessageDialogBuilder setFriends(List friends) { + return this; + } } private static abstract class OpenGraphDialogBuilderBase> @@ -1597,7 +1736,7 @@ public OpenGraphActionDialogBuilder(Activity activity, OpenGraphAction action, S } @Override - EnumSet getDialogFeatures() { + protected EnumSet getDialogFeatures() { return EnumSet.of(OpenGraphActionDialogFeature.OG_ACTION_DIALOG); } } @@ -1627,7 +1766,7 @@ public OpenGraphMessageDialogBuilder(Activity activity, OpenGraphAction action, } @Override - EnumSet getDialogFeatures() { + protected EnumSet getDialogFeatures() { return EnumSet.of(OpenGraphMessageDialogFeature.OG_MESSAGE_DIALOG); } } diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/LikeView.java b/platforms/android/FacebookLib/src/com/facebook/widget/LikeView.java new file mode 100644 index 000000000..8b1b8280e --- /dev/null +++ b/platforms/android/FacebookLib/src/com/facebook/widget/LikeView.java @@ -0,0 +1,693 @@ +/** + * 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.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.facebook.android.R; +import com.facebook.internal.*; + +/** + * This class provides the UI for displaying the Facebook Like button and its associated components. + */ +public class LikeView extends FrameLayout { + + // *** + // Keep all the enum values in sync with attrs.xml + // *** + + /** + * Encapsulates the valid values for the facebook:style attribute for a LikeView + */ + public enum Style { + /** + * Setting the attribute to this value will display the button and a sentence near it that describes the + * social sentence for the associated object. + * + * This is the default value + */ + STANDARD("standard", 0), + + /** + * Setting the attribute to this value will display the button by itself, with no other components + */ + BUTTON("button", 1), + + /** + * Setting the attribute to this value will display the button and a box near it with the number of likes + * for the associated object + */ + BOX_COUNT("box_count", 2); + + static Style DEFAULT = STANDARD; + + static Style fromInt(int enumValue) { + for (Style style : values()) { + if (style.getValue() == enumValue) { + return style; + } + } + + return null; + } + + private String stringValue; + private int intValue; + private Style(String stringValue, int value) { + this.stringValue = stringValue; + this.intValue = value; + } + + @Override + public String toString() { + return stringValue; + } + + private int getValue() { + return intValue; + } + } + + /** + * Encapsulates the valid values for the facebook:horizontal_alignment attribute for a LikeView. + */ + public enum HorizontalAlignment { + /** + * Setting the attribute to this value will center the button and auxiliary view in the parent view. + * + * This is the default value + */ + CENTER("center", 0), + + /** + * Setting the attribute to this value will left-justify the button and auxiliary view in the parent view. + */ + LEFT("left", 1), + + /** + * Setting the attribute to this value will right-justify the button and auxiliary view in the parent view. + * If the facebook:auxiliary_view_position is set to INLINE, then the auxiliary view will be on the + * left of the button + */ + RIGHT("right", 2); + + static HorizontalAlignment DEFAULT = CENTER; + + static HorizontalAlignment fromInt(int enumValue) { + for (HorizontalAlignment horizontalAlignment : values()) { + if (horizontalAlignment.getValue() == enumValue) { + return horizontalAlignment; + } + } + + return null; + } + + private String stringValue; + private int intValue; + private HorizontalAlignment(String stringValue, int value) { + this.stringValue = stringValue; + this.intValue = value; + } + + @Override + public String toString() { + return stringValue; + } + + private int getValue() { + return intValue; + } + } + + /** + * Encapsulates the valid values for the facebook:auxiliary_view_position attribute for a LikeView. + */ + public enum AuxiliaryViewPosition { + /** + * Setting the attribute to this value will put the social-sentence or box-count below the like button. + * If the facebook:style is set to BUTTON, then this has no effect. + * + * This is the default value + */ + BOTTOM("bottom", 0), + + /** + * Setting the attribute to this value will put the social-sentence or box-count inline with the like button. + * The auxiliary view will be to the left of the button if the facebook:horizontal_alignment is set to RIGHT. + * In all other cases, it will be to the right of the button. + * If the facebook:style is set to BUTTON, then this has no effect. + */ + INLINE("inline", 1), + + /** + * Setting the attribute to this value will put the social-sentence or box-count above the like button. + * If the facebook:style is set to BUTTON, then this has no effect. + */ + TOP("top", 2); + + static AuxiliaryViewPosition DEFAULT = BOTTOM; + + static AuxiliaryViewPosition fromInt(int enumValue) { + for (AuxiliaryViewPosition auxViewPosition : values()) { + if (auxViewPosition.getValue() == enumValue) { + return auxViewPosition; + } + } + + return null; + } + + private String stringValue; + private int intValue; + private AuxiliaryViewPosition(String stringValue, int value) { + this.stringValue = stringValue; + this.intValue = value; + } + + @Override + public String toString() { + return stringValue; + } + + private int getValue() { + return intValue; + } + } + + private static final int NO_FOREGROUND_COLOR = -1; + + private String objectId; + private LinearLayout containerView; + private LikeButton likeButton; + private LikeBoxCountView likeBoxCountView; + private TextView socialSentenceView; + private LikeActionController likeActionController; + private OnErrorListener onErrorListener; + private BroadcastReceiver broadcastReceiver; + private LikeActionControllerCreationCallback creationCallback; + + private Style likeViewStyle = Style.DEFAULT; + private HorizontalAlignment horizontalAlignment = HorizontalAlignment.DEFAULT; + private AuxiliaryViewPosition auxiliaryViewPosition = AuxiliaryViewPosition.DEFAULT; + private int foregroundColor = NO_FOREGROUND_COLOR; + + private int edgePadding; + private int internalPadding; + + /** + * If your app does not use UiLifeCycleHelper, then you must call this method in the calling activity's + * onActivityResult method, to process any pending like actions, where tapping the button had resulted in + * the Like dialog being shown in the Facebook application. + * + * @param context Hosting context + * @param requestCode From the originating call to onActivityResult + * @param resultCode From the originating call to onActivityResult + * @param data From the originating call to onActivityResult + * @return Indication of whether the Intent was handled + */ + public static boolean handleOnActivityResult(Context context, + int requestCode, + int resultCode, + Intent data) { + return LikeActionController.handleOnActivityResult(context, requestCode, resultCode, data); + } + + /** + * Constructor + * + * @param context Context for this View + */ + public LikeView(Context context) { + super(context); + initialize(context); + } + + /** + * Constructor + * + * @param context Context for this View + * @param attrs AttributeSet for this View. + */ + public LikeView(Context context, AttributeSet attrs) { + super(context, attrs); + parseAttributes(attrs); + initialize(context); + } + + /** + * Sets the associated object for this LikeView. Can be changed during runtime. + * @param objectId Object Id + */ + public void setObjectId(String objectId) { + objectId = Utility.coerceValueIfNullOrEmpty(objectId, null); + if (!Utility.areObjectsEqual(objectId, this.objectId)) { + setObjectIdForced(objectId); + + updateLikeStateAndLayout(); + } + } + + /** + * Sets the facebook:style for this LikeView. Can be changed during runtime. + * @param likeViewStyle Should be either LikeView.STANDARD, LikeView.BUTTON or LikeView.BOX_COUNT + */ + public void setLikeViewStyle(Style likeViewStyle) { + likeViewStyle = likeViewStyle != null ? likeViewStyle : Style.DEFAULT; + if (this.likeViewStyle != likeViewStyle) { + this.likeViewStyle = likeViewStyle; + + updateLayout(); + } + } + + /** + * Sets the facebook:auxiliary_view_position for this LikeView. Can be changed during runtime. + * @param auxiliaryViewPosition Should be either LikeView.TOP, LikeView.INLINE or LikeView.BOTTOM + */ + public void setAuxiliaryViewPosition(AuxiliaryViewPosition auxiliaryViewPosition) { + auxiliaryViewPosition = auxiliaryViewPosition != null ? auxiliaryViewPosition : AuxiliaryViewPosition.DEFAULT; + if (this.auxiliaryViewPosition != auxiliaryViewPosition) { + this.auxiliaryViewPosition = auxiliaryViewPosition; + + updateLayout(); + } + } + + /** + * Sets the facebook:horizontal_alignment for this LikeView. Can be changed during runtime. + * @param horizontalAlignment Should be either LikeView.LEFT, LikeView.CENTER or LikeView.RIGHT + */ + public void setHorizontalAlignment(HorizontalAlignment horizontalAlignment) { + horizontalAlignment = horizontalAlignment != null ? horizontalAlignment : HorizontalAlignment.DEFAULT; + if (this.horizontalAlignment != horizontalAlignment) { + this.horizontalAlignment = horizontalAlignment; + + updateLayout(); + } + } + + /** + * Sets the facebook:foreground_color for this LikeView. Can be changed during runtime. + * The color is only used for the social sentence text. + * @param foregroundColor And valid android.graphics.Color value. + */ + public void setForegroundColor(int foregroundColor) { + if (this.foregroundColor != foregroundColor) { + socialSentenceView.setTextColor(foregroundColor); + } + } + + /** + * 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) { + this.onErrorListener = onErrorListener; + } + + /** + * Returns the current OnErrorListener for this instance of LoginButton. + * + * @return The OnErrorListener + */ + public OnErrorListener getOnErrorListener() { + return onErrorListener; + } + + @Override + protected void onDetachedFromWindow() { + // Disassociate from the object + setObjectId(null); + + super.onDetachedFromWindow(); + } + + private void parseAttributes(AttributeSet attrs) { + if (attrs == null || getContext() == null) { + return; + } + + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.com_facebook_like_view); + if (a == null) { + return; + } + + objectId = Utility.coerceValueIfNullOrEmpty(a.getString(R.styleable.com_facebook_like_view_object_id), null); + likeViewStyle = Style.fromInt( + a.getInt(R.styleable.com_facebook_like_view_style, + Style.DEFAULT.getValue())); + if (likeViewStyle == null) { + throw new IllegalArgumentException("Unsupported value for LikeView 'style'"); + } + + auxiliaryViewPosition = AuxiliaryViewPosition.fromInt( + a.getInt(R.styleable.com_facebook_like_view_auxiliary_view_position, + AuxiliaryViewPosition.DEFAULT.getValue())); + if (auxiliaryViewPosition == null) { + throw new IllegalArgumentException("Unsupported value for LikeView 'auxiliary_view_position'"); + } + + horizontalAlignment = HorizontalAlignment.fromInt( + a.getInt(R.styleable.com_facebook_like_view_horizontal_alignment, + HorizontalAlignment.DEFAULT.getValue())); + if (horizontalAlignment == null) { + throw new IllegalArgumentException("Unsupported value for LikeView 'horizontal_alignment'"); + } + + foregroundColor = a.getColor(R.styleable.com_facebook_like_view_foreground_color, NO_FOREGROUND_COLOR); + + a.recycle(); + } + + // If attributes were present, parseAttributes MUST be called before initialize() to ensure proper behavior + private void initialize(Context context) { + edgePadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeview_edge_padding); + internalPadding = getResources().getDimensionPixelSize(R.dimen.com_facebook_likeview_internal_padding); + if (foregroundColor == NO_FOREGROUND_COLOR) { + foregroundColor = getResources().getColor(R.color.com_facebook_likeview_text_color); + } + + setBackgroundColor(Color.TRANSPARENT); + + containerView = new LinearLayout(context); + LayoutParams containerViewLayoutParams = new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + containerView.setLayoutParams(containerViewLayoutParams); + + initializeLikeButton(context); + initializeSocialSentenceView(context); + initializeLikeCountView(context); + + containerView.addView(likeButton); + containerView.addView(socialSentenceView); + containerView.addView(likeBoxCountView); + + addView(containerView); + + setObjectIdForced(this.objectId); + updateLikeStateAndLayout(); + } + + private void initializeLikeButton(Context context) { + likeButton = new LikeButton( + context, + likeActionController != null ? likeActionController.isObjectLiked() : false); + likeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + toggleLike(); + } + }); + + LinearLayout.LayoutParams buttonLayout = new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + + likeButton.setLayoutParams(buttonLayout); + } + + private void initializeSocialSentenceView(Context context) { + socialSentenceView = new TextView(context); + socialSentenceView.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.com_facebook_likeview_text_size)); + socialSentenceView.setMaxLines(2); + socialSentenceView.setTextColor(foregroundColor); + socialSentenceView.setGravity(Gravity.CENTER); + + LinearLayout.LayoutParams socialSentenceViewLayout = new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT); + socialSentenceView.setLayoutParams(socialSentenceViewLayout); + } + + private void initializeLikeCountView(Context context) { + likeBoxCountView = new LikeBoxCountView(context); + + LinearLayout.LayoutParams likeCountViewLayout = new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + likeBoxCountView.setLayoutParams(likeCountViewLayout); + } + + private void toggleLike() { + if (likeActionController != null) { + Activity activity = (Activity)getContext(); + likeActionController.toggleLike(activity, getAnalyticsParameters()); + } + } + + private Bundle getAnalyticsParameters() { + Bundle params = new Bundle(); + params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_STYLE, likeViewStyle.toString()); + params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_AUXILIARY_POSITION, auxiliaryViewPosition.toString()); + params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_HORIZONTAL_ALIGNMENT, horizontalAlignment.toString()); + params.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_ID, Utility.coerceValueIfNullOrEmpty(objectId, "")); + return params; + } + + private void setObjectIdForced(String newObjectId) { + tearDownObjectAssociations(); + + objectId = newObjectId; + if (Utility.isNullOrEmpty(newObjectId)) { + return; + } + + creationCallback = new LikeActionControllerCreationCallback(); + LikeActionController.getControllerForObjectId( + getContext(), + newObjectId, + creationCallback); + } + + private void associateWithLikeActionController(LikeActionController likeActionController) { + this.likeActionController = likeActionController; + + this.broadcastReceiver = new LikeControllerBroadcastReceiver(); + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext()); + + // add the broadcast receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_UPDATED); + filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR); + filter.addAction(LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET); + + localBroadcastManager.registerReceiver(broadcastReceiver, filter); + } + + private void tearDownObjectAssociations() { + if (broadcastReceiver != null) { + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getContext()); + localBroadcastManager.unregisterReceiver(broadcastReceiver); + + broadcastReceiver = null; + } + + // If we were already waiting on a controller to be given back, make sure we aren't waiting anymore. + // Otherwise when that controller is given back to the callback, it will go and register a broadcast receiver + // for it. + if (creationCallback != null) { + creationCallback.cancel(); + + creationCallback = null; + } + + likeActionController = null; + } + + private void updateLikeStateAndLayout() { + if (likeActionController == null) { + likeButton.setLikeState(false); + socialSentenceView.setText(null); + likeBoxCountView.setText(null); + } else { + likeButton.setLikeState(likeActionController.isObjectLiked()); + socialSentenceView.setText(likeActionController.getSocialSentence()); + likeBoxCountView.setText(likeActionController.getLikeCountString()); + } + + updateLayout(); + } + + private void updateLayout() { + // Make sure the container is horizontally aligned according to specifications. + LayoutParams containerViewLayoutParams = (LayoutParams)containerView.getLayoutParams(); + LinearLayout.LayoutParams buttonLayoutParams = (LinearLayout.LayoutParams)likeButton.getLayoutParams(); + int viewGravity = + horizontalAlignment == HorizontalAlignment.LEFT ? Gravity.LEFT : + horizontalAlignment == HorizontalAlignment.CENTER ? Gravity.CENTER_HORIZONTAL : Gravity.RIGHT; + + containerViewLayoutParams.gravity = viewGravity | Gravity.TOP; + buttonLayoutParams.gravity = viewGravity; + + // Choose the right auxiliary view to make visible. + socialSentenceView.setVisibility(GONE); + likeBoxCountView.setVisibility(GONE); + + View auxView; + if (likeViewStyle == Style.STANDARD && + likeActionController != null && + !Utility.isNullOrEmpty(likeActionController.getSocialSentence())) { + auxView = socialSentenceView; + } else if (likeViewStyle == Style.BOX_COUNT && + likeActionController != null && + !Utility.isNullOrEmpty(likeActionController.getLikeCountString())) { + updateBoxCountCaretPosition(); + auxView = likeBoxCountView; + } else { + // No more work to be done. + return; + } + auxView.setVisibility(VISIBLE); + + // Now position the auxiliary view properly + LinearLayout.LayoutParams auxViewLayoutParams = (LinearLayout.LayoutParams)auxView.getLayoutParams(); + auxViewLayoutParams.gravity = viewGravity; + + containerView.setOrientation( + auxiliaryViewPosition == AuxiliaryViewPosition.INLINE ? + LinearLayout.HORIZONTAL : + LinearLayout.VERTICAL); + + if (auxiliaryViewPosition == AuxiliaryViewPosition.TOP || + (auxiliaryViewPosition == AuxiliaryViewPosition.INLINE && + horizontalAlignment == HorizontalAlignment.RIGHT)) { + // Button comes after the auxiliary view. Make sure it is at the end + containerView.removeView(likeButton); + containerView.addView(likeButton); + } else { + // In all other cases, the button comes first + containerView.removeView(auxView); + containerView.addView(auxView); + } + + switch (auxiliaryViewPosition) { + case TOP: + auxView.setPadding(edgePadding, edgePadding, edgePadding, internalPadding); + break; + case BOTTOM: + auxView.setPadding(edgePadding, internalPadding, edgePadding, edgePadding); + break; + case INLINE: + if (horizontalAlignment == HorizontalAlignment.RIGHT) { + auxView.setPadding(edgePadding, edgePadding, internalPadding, edgePadding); + } else { + auxView.setPadding(internalPadding, edgePadding, edgePadding, edgePadding); + } + break; + } + } + + private void updateBoxCountCaretPosition() { + switch (auxiliaryViewPosition) { + case TOP: + likeBoxCountView.setCaretPosition(LikeBoxCountView.LikeBoxCountViewCaretPosition.BOTTOM); + break; + case BOTTOM: + likeBoxCountView.setCaretPosition(LikeBoxCountView.LikeBoxCountViewCaretPosition.TOP); + break; + case INLINE: + likeBoxCountView.setCaretPosition( + horizontalAlignment == HorizontalAlignment.RIGHT ? + LikeBoxCountView.LikeBoxCountViewCaretPosition.RIGHT : + LikeBoxCountView.LikeBoxCountViewCaretPosition.LEFT); + break; + } + } + + /** + * 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 errorBundle a FacebookException representing the error that was encountered. + */ + void onError(Bundle errorBundle); + } + + private class LikeControllerBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String intentAction = intent.getAction(); + Bundle extras = intent.getExtras(); + boolean shouldRespond = true; + if (extras != null) { + // See if an Id was set in the broadcast Intent. If it was, treat it as a filter. + String broadcastObjectId = extras.getString(LikeActionController.ACTION_OBJECT_ID_KEY); + shouldRespond = Utility.isNullOrEmpty(broadcastObjectId) || + Utility.areObjectsEqual(objectId, broadcastObjectId); + } + + if (!shouldRespond) { + return; + } + + if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_UPDATED.equals(intentAction)) { + updateLikeStateAndLayout(); + } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR.equals(intentAction)) { + if (onErrorListener != null) { + onErrorListener.onError(extras); + } + } else if (LikeActionController.ACTION_LIKE_ACTION_CONTROLLER_DID_RESET.equals(intentAction)) { + // This will recreate the controller and associated objects + setObjectIdForced(objectId); + updateLikeStateAndLayout(); + } + } + } + + private class LikeActionControllerCreationCallback implements LikeActionController.CreationCallback { + private boolean isCancelled; + + public void cancel() { + isCancelled = true; + } + + @Override + public void onComplete(LikeActionController likeActionController) { + if (isCancelled) { + return; + } + + associateWithLikeActionController(likeActionController); + updateLikeStateAndLayout(); + + LikeView.this.creationCallback = null; + } + } +} diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java b/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java index 6f548f4dc..8b82e2d21 100644 --- a/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java +++ b/platforms/android/FacebookLib/src/com/facebook/widget/LoginButton.java @@ -381,7 +381,7 @@ public void setReadPermissions(String... permissions) { * manage the setting of permissions outside of the LoginButton class altogether * (by managing the session explicitly). * - * @param permissions the read permissions to use + * @param permissions the publish permissions to use * * @throws UnsupportedOperationException if setReadPermissions has been called * @throws IllegalArgumentException if permissions is null or empty @@ -407,7 +407,7 @@ public void setPublishPermissions(List permissions) { * manage the setting of permissions outside of the LoginButton class altogether * (by managing the session explicitly). * - * @param permissions the read permissions to use + * @param permissions the publish permissions to use * * @throws UnsupportedOperationException if setReadPermissions has been called * @throws IllegalArgumentException if permissions is null or empty diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java b/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java index 4ef6cbd23..536c0c783 100644 --- a/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java +++ b/platforms/android/FacebookLib/src/com/facebook/widget/PickerFragment.java @@ -97,6 +97,7 @@ public abstract class PickerFragment extends Fragment { private final Class graphObjectClass; private LoadingStrategy loadingStrategy; private SelectionStrategy selectionStrategy; + private Set selectionHint; private ProgressBar activityCircle; private SessionTracker sessionTracker; private String titleText; @@ -487,9 +488,21 @@ public String getDoneButtonText() { * if false, data will not be re-loaded if it is already displayed (or loading) */ public void loadData(boolean forceReload) { + loadData(forceReload, null); + } + + /** + * 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) + * @param selectIds ids to select, if they are present in the loaded data + */ + public void loadData(boolean forceReload, Set selectIds) { if (!forceReload && loadingStrategy.isDataPresentOrLoading()) { return; } + selectionHint = selectIds; loadDataSkippingRoundTripIfCached(); } @@ -751,6 +764,32 @@ void updateAdapter(SimpleGraphObjectCursor data) { if (dataChanged && onDataChangedListener != null) { onDataChangedListener.onDataChanged(PickerFragment.this); } + if (selectionHint != null && !selectionHint.isEmpty() && data != null) { + data.moveToFirst(); + boolean changed = false; + for (int i = 0; i < data.getCount(); i++) { + data.moveToPosition(i); + T graphObject = data.getGraphObject(); + if (!graphObject.asMap().containsKey("id")) + continue; + Object obj = graphObject.getProperty("id"); + if (!(obj instanceof String)) { + continue; + } + String id = (String) obj; + if (selectionHint.contains(id)) { + selectionStrategy.toggleSelection(id); + selectionHint.remove(id); + changed = true; + } + if (selectionHint.isEmpty()) { + break; + } + } + if (onSelectionChangedListener != null && changed) { + onSelectionChangedListener.onSelectionChanged(PickerFragment.this); + } + } } } diff --git a/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java b/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java index 9150f50a5..cf54a850a 100644 --- a/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java +++ b/platforms/android/FacebookLib/src/com/facebook/widget/WebDialog.java @@ -38,7 +38,6 @@ import android.widget.LinearLayout; import com.facebook.*; import com.facebook.android.R; -import com.facebook.android.Util; import com.facebook.internal.Logger; import com.facebook.internal.ServerProtocol; import com.facebook.internal.Utility; @@ -52,6 +51,7 @@ 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 int API_EC_DIALOG_CANCEL = 4201; static final String REDIRECT_URI = "fbconnect://success"; static final String CANCEL_URI = "fbconnect://cancel"; static final boolean DISABLE_SSL_CHECK_FOR_TESTING = false; @@ -73,6 +73,7 @@ public class WebDialog extends Dialog { public static final int DEFAULT_THEME = android.R.style.Theme_Translucent_NoTitleBar; private String url; + private String expectedRedirectUrl = REDIRECT_URI; private OnCompleteListener onCompleteListener; private WebView webView; private ProgressDialog spinner; @@ -80,6 +81,7 @@ public class WebDialog extends Dialog { private FrameLayout contentFrameLayout; private boolean listenerCalled = false; private boolean isDetached = false; + private boolean isDismissed = false; /** * Interface that implements a listener to be called when the user's interaction with the @@ -169,6 +171,19 @@ public OnCompleteListener getOnCompleteListener() { @Override public void dismiss() { + if (isDismissed) { + // Some paths may cause dismiss() to be called recursively. Break the loop here. + return; + } + isDismissed = true; + + // If dismiss() was called without sending a result to the listener, let's default to a "cancel" result. + // This will be the case when the user taps the OS-back-button, or the cross-image, or outside the loading + // interstitial. + if (!listenerCalled) { + sendCancelToListener(); + } + if (webView != null) { webView.stopLoading(); } @@ -196,21 +211,13 @@ public void onAttachedToWindow() { 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(); + dismiss(); } }); @@ -246,6 +253,27 @@ public void onCancel(DialogInterface dialogInterface) { setContentView(contentFrameLayout); } + protected void setExpectedRedirectUrl(String expectedRedirectUrl) { + this.expectedRedirectUrl = expectedRedirectUrl; + } + + protected Bundle parseResponseUri(String urlString) { + Uri u = Uri.parse(urlString); + + Bundle b = Utility.parseUrlQueryString(u.getQuery()); + b.putAll(Utility.parseUrlQueryString(u.getFragment())); + + return b; + } + + protected boolean isListenerCalled() { + return listenerCalled; + } + + protected WebView getWebView() { + return webView; + } + private void calculateSize() { WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); @@ -292,14 +320,16 @@ private int getScaledSize(int screenSize, float density, int noPaddingSize, int return (int) (screenSize * scaleFactor); } - private void sendSuccessToListener(Bundle values) { + protected void sendSuccessToListener(Bundle values) { if (onCompleteListener != null && !listenerCalled) { listenerCalled = true; onCompleteListener.onComplete(values, null); + + dismiss(); } } - private void sendErrorToListener(Throwable error) { + protected void sendErrorToListener(Throwable error) { if (onCompleteListener != null && !listenerCalled) { listenerCalled = true; FacebookException facebookException = null; @@ -309,10 +339,12 @@ private void sendErrorToListener(Throwable error) { facebookException = new FacebookException(error); } onCompleteListener.onComplete(null, facebookException); + + dismiss(); } } - private void sendCancelToListener() { + protected void sendCancelToListener() { sendErrorToListener(new FacebookOperationCanceledException()); } @@ -322,8 +354,7 @@ private void createCrossImage() { crossImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - sendCancelToListener(); - WebDialog.this.dismiss(); + dismiss(); } }); Drawable crossDrawable = getContext().getResources().getDrawable(R.drawable.com_facebook_close); @@ -337,7 +368,18 @@ public void onClick(View v) { @SuppressLint("SetJavaScriptEnabled") private void setUpWebView(int margin) { LinearLayout webViewContainer = new LinearLayout(getContext()); - webView = new WebView(getContext()); + webView = new WebView(getContext()) { + /* Prevent NPE on Motorola 2.2 devices + * See https://groups.google.com/forum/?fromgroups=#!topic/android-developers/ktbwY2gtLKQ + */ + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + try { + super.onWindowFocusChanged(hasWindowFocus); + } catch (NullPointerException e) { + } + } + }; webView.setVerticalScrollBarEnabled(false); webView.setHorizontalScrollBarEnabled(false); webView.setWebViewClient(new DialogWebViewClient()); @@ -360,8 +402,8 @@ private class DialogWebViewClient extends WebViewClient { @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); + if (url.startsWith(WebDialog.this.expectedRedirectUrl)) { + Bundle values = parseResponseUri(url); String error = values.getString("error"); if (error == null) { @@ -388,16 +430,15 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { } else if (error != null && (error.equals("access_denied") || error.equals("OAuthAccessDeniedException"))) { sendCancelToListener(); + } else if (errorCode == API_EC_DIALOG_CANCEL) { + 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; @@ -413,7 +454,6 @@ 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 @@ -423,9 +463,8 @@ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError e } else { super.onReceivedSslError(view, handler, error); - sendErrorToListener(new FacebookDialogException(null, ERROR_FAILED_SSL_HANDSHAKE, null)); handler.cancel(); - WebDialog.this.dismiss(); + sendErrorToListener(new FacebookDialogException(null, ERROR_FAILED_SSL_HANDSHAKE, null)); } } @@ -574,7 +613,7 @@ private void finishInit(Context context, String action, Bundle parameters) { } /** - * Provides a builder that allows construction of an arbitary Facebook web dialog. + * Provides a builder that allows construction of an arbitrary Facebook web dialog. */ public static class Builder extends BuilderBase { /** @@ -746,7 +785,7 @@ public FeedDialogBuilder setSource(String source) { } /** - * Sets the name of the item being shared. + * Sets the name of the URL being shared. This method only has effect if setLink is called. * * @param name the name * @return the builder @@ -757,7 +796,7 @@ public FeedDialogBuilder setName(String name) { } /** - * Sets the caption to be displayed. + * Sets the caption of the URL being shared. This method only has effect if setLink is called. * * @param caption the caption * @return the builder @@ -768,7 +807,7 @@ public FeedDialogBuilder setCaption(String caption) { } /** - * Sets the description to be displayed. + * Sets the description of the URL being shared. This method only has effect if setLink is called. * * @param description the description * @return the builder diff --git a/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java b/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java index 9e4fb06d2..35f7d58a6 100644 --- a/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java +++ b/platforms/android/src/org/apache/cordova/facebook/ConnectPlugin.java @@ -553,7 +553,21 @@ private void handleSuccess(Bundle values) { try { Set keys = values.keySet(); for (String key : keys) { - response.put(key, values.get(key)); + //check if key is array + int beginArrayCharIndex = key.indexOf("["); + if (beginArrayCharIndex >= 0) { + String normalizedKey = key.substring(0, beginArrayCharIndex); + JSONArray result; + if (response.has(normalizedKey)) { + result = (JSONArray) response.get(normalizedKey); + } else { + result = new JSONArray(); + response.put(normalizedKey, result); + } + result.put(result.length(), values.get(key)); + } else { + response.put(key, values.get(key)); + } } } catch (JSONException e) { e.printStackTrace(); @@ -726,7 +740,7 @@ public JSONObject getFacebookRequestErrorResponse(FacebookRequestError error) { response += ",\"errorUserMessage\": \"" + cordova.getActivity().getResources().getString(error.getUserActionMessageId()) + "\""; } } - + response += "}"; try { diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAccessTokenData.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAccessTokenData.h index 23aa6af6f..2c6c98a2d 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAccessTokenData.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAccessTokenData.h @@ -123,6 +123,7 @@ @param refreshDate The date that token was last refreshed. A value of nil defaults to `[NSDate date]`. @param permissionsRefreshDate The date the permissions were last refreshed. A value of nil defaults to `[NSDate distantPast]`. @param appID The ID string of the calling app. A value of nil defaults to `[FBSettings defaultAppID]`. + @param userID The user ID associated with the token. */ + (FBAccessTokenData *)createTokenFromString:(NSString *)accessToken permissions:(NSArray *)permissions diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAppEvents.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAppEvents.h index 9bf395373..01305f00e 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAppEvents.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBAppEvents.h @@ -401,7 +401,9 @@ FBSDK_EXTERN NSString *const FBAppEventParameterValueNo; /*! @method - @abstract This method has been replaced by [FBSettings setLimitEventUsage] */ + @abstract This method has been replaced by [FBSettings setLimitEventUsage] + @param limitEventUsage deprecated +*/ + (void)setLimitEventUsage:(BOOL)limitEventUsage __attribute__ ((deprecated("use [FBSettings setLimitEventAndDataUsage] instead"))); /*! @@ -475,7 +477,7 @@ FBSDK_EXTERN NSString *const FBAppEventParameterValueNo; Get the 'override' App ID for App Event logging. @discussion - @see `setLoggingOverrideAppID:` + @see setLoggingOverrideAppID: */ + (NSString *)loggingOverrideAppID; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBDialogs.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBDialogs.h index 4c796c063..01d34506b 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBDialogs.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBDialogs.h @@ -220,7 +220,7 @@ typedef void (^FBDialogAppCallCompletionHandler)( @see canPresentShareDialogWithOpenGraphActionParams: @see canPresentShareDialogWithParams: - @see canPresentShareDialogWithPhotos: + @see canPresentShareDialogWithPhotos */ + (BOOL)canPresentShareDialog; @@ -633,7 +633,7 @@ typedef void (^FBDialogAppCallCompletionHandler)( @see canPresentMessageDialogWithOpenGraphActionParams: @see canPresentMessageDialogWithParams: - @see canPresentMessageDialogWithPhotos: + @see canPresentMessageDialogWithPhotos */ + (BOOL)canPresentMessageDialog; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBErrorUtility.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBErrorUtility.h index 76225ecf9..3f346a8b9 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBErrorUtility.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBErrorUtility.h @@ -35,7 +35,7 @@ The error category can be used to understand the class of error received from Facebook. For more infomation on this see https://developers.facebook.com/docs/reference/api/errors/ - @param error the error to be categorized. + @param error The error to be categorized. */ + (FBErrorCategory)errorCategoryForError:(NSError *)error; @@ -49,7 +49,7 @@ take an action before the application continues to attempt a Facebook connection. For more infomation on this see https://developers.facebook.com/docs/reference/api/errors/ - @param error the error to inspect. + @param error The error to inspect. */ + (BOOL)shouldNotifyUserForError:(NSError *)error; @@ -59,7 +59,7 @@ Not all Facebook errors yield a message suitable for user display; however in all cases where +shouldNotifyUserForError: returns YES, this method returns a localizable message suitable for display. - @param error the error to inspect. + @param error The error to inspect. */ + (NSString *)userMessageForError:(NSError *)error; @@ -69,7 +69,7 @@ Not all Facebook errors yield a localized message/title suitable for user display; however in all cases when title is available, user should be notified. - @param error the error to inspect. + @param error The error to inspect. */ + (NSString *)userTitleForError:(NSError *)error; @@ -77,6 +77,8 @@ @abstract YES if given error is transient and may succeed if the initial action is retried as-is. Application may use this information to display a "Retry" button, if user should be notified about this error. + + @param error The error to inspect. */ + (BOOL)isTransientError:(NSError *)error; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBFriendPickerViewController.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBFriendPickerViewController.h index 29ffb5fcc..b3e270442 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBFriendPickerViewController.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBFriendPickerViewController.h @@ -51,7 +51,7 @@ /*! @abstract - The list of friends that are currently selected in the veiw. + The list of friends that are currently selected in the view. The items in the array are objects. @discussion diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBInsights.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBInsights.h index 4b7432186..c178536e1 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBInsights.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBInsights.h @@ -26,8 +26,10 @@ */ __attribute__ ((deprecated("use FBAppEventsFlushBehavior instead"))) typedef NS_ENUM(NSUInteger, FBInsightsFlushBehavior) { - FBInsightsFlushBehaviorAuto __attribute__ ((deprecated("use FBAppEventsFlushBehaviorAuto instead"))), - FBInsightsFlushBehaviorExplicitOnly __attribute__ ((deprecated("use FBAppEventsFlushBehaviorExplicitOnly instead"))), + /*! @deprecated use FBAppEventsFlushBehaviorAuto instead */ + FBInsightsFlushBehaviorAuto = 0, + /*! @deprecated use FBAppEventsFlushBehaviorExplicitOnly instead */ + FBInsightsFlushBehaviorExplicitOnly }; FBSDK_EXTERN NSString *const FBInsightsLoggingResultNotification __attribute__((deprecated)); @@ -40,19 +42,62 @@ FBSDK_EXTERN NSString *const FBInsightsLoggingResultNotification __attribute__(( __attribute__ ((deprecated("Use the FBAppEvents class instead"))) @interface FBInsights : NSObject +/*! + @abstract deprecated +*/ + (NSString *)appVersion __attribute__((deprecated)); +/*! + @abstract deprecated + @param appVersion deprecated +*/ + (void)setAppVersion:(NSString *)appVersion __attribute__((deprecated("use [FBSettings setAppVersion] instead"))); +/*! + @abstract deprecated + @param purchaseAmount deprecated + @param currency deprecated + */ + (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency __attribute__((deprecated("use [FBAppEvents logPurchase] instead"))); +/*! + @abstract deprecated + @param purchaseAmount deprecated + @param currency deprecated + @param parameters deprecated + */ + (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency parameters:(NSDictionary *)parameters __attribute__((deprecated("use [FBAppEvents logPurchase] instead"))); +/*! + @abstract deprecated + @param purchaseAmount deprecated + @param currency deprecated + @param parameters deprecated + @param session deprecated + */ + (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency parameters:(NSDictionary *)parameters session:(FBSession *)session __attribute__((deprecated("use [FBAppEvents logPurchase] instead"))); - +/*! + @abstract deprecated + @param pixelID deprecated + @param value deprecated + */ + (void)logConversionPixel:(NSString *)pixelID valueOfPixel:(double)value __attribute__((deprecated)); +/*! + @abstract deprecated + @param pixelID deprecated + @param value deprecated + @param session deprecated + */ + (void)logConversionPixel:(NSString *)pixelID valueOfPixel:(double)value session:(FBSession *)session __attribute__((deprecated)); - +/*! + @abstract deprecated + */ + (FBInsightsFlushBehavior)flushBehavior __attribute__((deprecated("use [FBAppEvents flushBehavior] instead"))); +/*! + @abstract deprecated + @param flushBehavior deprecated + */ + (void)setFlushBehavior:(FBInsightsFlushBehavior)flushBehavior __attribute__((deprecated("use [FBAppEvents setFlushBehavior] instead"))); - +/*! + @abstract deprecated + */ + (void)flush __attribute__((deprecated("use [FBAppEvents flush] instead"))); @end diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBLikeControl.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBLikeControl.h index ad9e3a92e..511bf3b70 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBLikeControl.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBLikeControl.h @@ -59,6 +59,26 @@ typedef NS_ENUM(NSUInteger, FBLikeControlHorizontalAlignment) */ FBSDK_EXTERN NSString *NSStringFromFBLikeControlHorizontalAlignment(FBLikeControlHorizontalAlignment horizontalAlignment); +/*! + @typedef NS_ENUM (NSUInteger, FBLikeControlObjectType) + + @abstract Specifies the type of object referenced by the objectID of a like control. + */ +typedef NS_ENUM(NSUInteger, FBLikeControlObjectType) +{ + /*! The objectID refers to an unknown object type. */ + FBLikeControlObjectTypeUnknown = 0, + /*! The objectID refers to an Open Graph object. */ + FBLikeControlObjectTypeOpenGraphObject, + /*! The objectID refers to a Page object. */ + FBLikeControlObjectTypePage, +}; + +/*! + @abstract Converts an FBLikeControlObjectType to an NSString. + */ +FBSDK_EXTERN NSString *NSStringFromFBLikeControlObjectType(FBLikeControlObjectType objectType); + /*! @typedef NS_ENUM (NSUInteger, FBLikeControlStyle) @@ -124,6 +144,15 @@ FBSDK_EXTERN NSString *NSStringFromFBLikeControlStyle(FBLikeControlStyle style); */ @property (nonatomic, copy) NSString *objectID; +/*! + @abstract The type of object referenced by the objectID. + + @discussion If the objectType is unknown, the control will determine the objectType by querying the server with the + objectID. Specifying a value for the objectType is an optimization that should be used if the type is known by the + consumer. Consider setting the objectType if you know your objectType when setting the objectID. + */ +@property (nonatomic, assign) FBLikeControlObjectType objectType; + /*! @abstract The preferred maximum width (in points) for autolayout. diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBNativeDialogs.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBNativeDialogs.h index d1c86b530..ddf31a102 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBNativeDialogs.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBNativeDialogs.h @@ -66,6 +66,11 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `presentOSIntegratedShareDialogModallyFrom`. + @param viewController deprecated + @param initialText deprecated + @param image deprecated + @param url deprecated + @param handler deprecated */ + (BOOL)presentShareDialogModallyFrom:(UIViewController *)viewController initialText:(NSString *)initialText @@ -78,6 +83,11 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `presentOSIntegratedShareDialogModallyFrom`. + @param viewController deprecated + @param initialText deprecated + @param images deprecated + @param urls deprecated + @param handler deprecated */ + (BOOL)presentShareDialogModallyFrom:(UIViewController *)viewController initialText:(NSString *)initialText @@ -90,6 +100,12 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `presentOSIntegratedShareDialogModallyFrom`. + @param viewController deprecated + @param session deprecated + @param initialText deprecated + @param images deprecated + @param urls deprecated + @param handler deprecated */ + (BOOL)presentShareDialogModallyFrom:(UIViewController *)viewController session:(FBSession *)session @@ -103,6 +119,7 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `canPresentOSIntegratedShareDialogWithSession`. + @param session deprecated */ + (BOOL)canPresentShareDialogWithSession:(FBSession *)session __attribute__((deprecated)); diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBRequestConnection.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBRequestConnection.h index 2f7cbae47..c5ef53752 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBRequestConnection.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBRequestConnection.h @@ -313,6 +313,10 @@ totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite; */ @property (nonatomic, assign) FBRequestConnectionErrorBehavior errorBehavior; +/*! + @abstract + The delegate object that receives updates. + */ @property (nonatomic, assign) id delegate; /*! diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBSettings.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBSettings.h index 242f49e1b..b2628f75e 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBSettings.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBSettings.h @@ -47,6 +47,9 @@ FBSDK_EXTERN NSString *const FBLoggingBehaviorInformational; /*! Log cache errors. */ FBSDK_EXTERN NSString *const FBLoggingBehaviorCacheErrors; +/*! Log errors from SDK UI controls */ +FBSDK_EXTERN NSString *const FBLoggingBehaviorUIControlErrors; + /*! Log errors likely to be preventable by the developer. This is in the default set of enabled logging behaviors. */ FBSDK_EXTERN NSString *const FBLoggingBehaviorDeveloperErrors; @@ -58,13 +61,13 @@ FBSDK_EXTERN NSString *const FBLoggingBehaviorDeveloperErrors; in release builds. */ typedef NS_ENUM(NSUInteger, FBBetaFeatures) { + /*! Default value indicating no beta features */ FBBetaFeaturesNone = 0, -#if defined(DEBUG) || defined(FB_BUILD_ONLY) -#endif }; /*! @typedef + @abstract Indicates if this app should be restricted */ typedef NS_ENUM(NSUInteger, FBRestrictedTreatment) { @@ -122,6 +125,7 @@ typedef NS_ENUM(NSUInteger, FBRestrictedTreatment) { @abstract This method is deprecated -- App Events favors using bundle identifiers to this. + @param appVersion deprecated */ + (void)setAppVersion:(NSString *)appVersion __attribute__ ((deprecated("App Events favors use of bundle identifiers for version identification."))); @@ -293,14 +297,18 @@ typedef NS_ENUM(NSUInteger, FBRestrictedTreatment) { /*! @method + @abstract Returns YES if the legacy Graph API mode is enabled */ + (BOOL)isPlatformCompatibilityEnabled; /*! @method + @abstract Configures the SDK to use the legacy platform. + @param enable indicates whether to use the legacy mode + @discussion Setting this flag has several effects: - FBRequests will target v1.0 of the Graph API. - Login will use the prior behavior without abilities to decline permission. diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTestSession.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTestSession.h index 73b5ec070..41fd0e200 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTestSession.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTestSession.h @@ -79,8 +79,12 @@ FBSDK_EXTERN NSString *kThirdTestUserTag; @property (readonly, copy) NSString *testAppID; /// The App Secret of the test app as configured in the plist. @property (readonly, copy) NSString *testAppSecret; -// Defaults to NO. If set to YES, reauthorize calls will fail with a nil token -// as if the user had cancelled it reauthorize. +/*! + @abstract + @discussion + Defaults to NO. If set to YES, reauthorize calls will fail with a nil token + as if the user had cancelled it reauthorize. +*/ @property (assign) BOOL disableReauthorize; /*! diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTooltipView.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTooltipView.h index 0dcae1b2f..4a92bf98e 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTooltipView.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FBTooltipView.h @@ -56,6 +56,7 @@ typedef NS_ENUM(NSUInteger, FBTooltipColorStyle) { /*! @abstract Gets or sets the amount of time in seconds the tooltip should be displayed. + @discussion Set this to zero to make the display permanent until explicitly dismissed. Defaults to six seconds. */ @@ -63,6 +64,7 @@ typedef NS_ENUM(NSUInteger, FBTooltipColorStyle) { /*! @abstract Gets or sets the color style after initialization. + @discussion Defaults to value passed to -initWithTagline:message:colorStyle:. */ @property (nonatomic, assign) FBTooltipColorStyle colorStyle; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FacebookSDK.h b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FacebookSDK.h index 60cf4c3b5..12928c8cc 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FacebookSDK.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/DeprecatedHeaders/FacebookSDK.h @@ -138,6 +138,6 @@ */ -#define FB_IOS_SDK_VERSION_STRING @"3.19" -#define FB_IOS_SDK_TARGET_PLATFORM_VERSION @"v2.1" +#define FB_IOS_SDK_VERSION_STRING @"3.20.0" +#define FB_IOS_SDK_TARGET_PLATFORM_VERSION @"v2.2" diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/FacebookSDK b/platforms/ios/FacebookSDK.framework/Versions/A/FacebookSDK index 5073d3667..527107dd0 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/FacebookSDK and b/platforms/ios/FacebookSDK.framework/Versions/A/FacebookSDK differ diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAccessTokenData.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAccessTokenData.h index 23aa6af6f..2c6c98a2d 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAccessTokenData.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAccessTokenData.h @@ -123,6 +123,7 @@ @param refreshDate The date that token was last refreshed. A value of nil defaults to `[NSDate date]`. @param permissionsRefreshDate The date the permissions were last refreshed. A value of nil defaults to `[NSDate distantPast]`. @param appID The ID string of the calling app. A value of nil defaults to `[FBSettings defaultAppID]`. + @param userID The user ID associated with the token. */ + (FBAccessTokenData *)createTokenFromString:(NSString *)accessToken permissions:(NSArray *)permissions diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAppEvents.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAppEvents.h index 9bf395373..01305f00e 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAppEvents.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBAppEvents.h @@ -401,7 +401,9 @@ FBSDK_EXTERN NSString *const FBAppEventParameterValueNo; /*! @method - @abstract This method has been replaced by [FBSettings setLimitEventUsage] */ + @abstract This method has been replaced by [FBSettings setLimitEventUsage] + @param limitEventUsage deprecated +*/ + (void)setLimitEventUsage:(BOOL)limitEventUsage __attribute__ ((deprecated("use [FBSettings setLimitEventAndDataUsage] instead"))); /*! @@ -475,7 +477,7 @@ FBSDK_EXTERN NSString *const FBAppEventParameterValueNo; Get the 'override' App ID for App Event logging. @discussion - @see `setLoggingOverrideAppID:` + @see setLoggingOverrideAppID: */ + (NSString *)loggingOverrideAppID; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBDialogs.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBDialogs.h index 4c796c063..01d34506b 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBDialogs.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBDialogs.h @@ -220,7 +220,7 @@ typedef void (^FBDialogAppCallCompletionHandler)( @see canPresentShareDialogWithOpenGraphActionParams: @see canPresentShareDialogWithParams: - @see canPresentShareDialogWithPhotos: + @see canPresentShareDialogWithPhotos */ + (BOOL)canPresentShareDialog; @@ -633,7 +633,7 @@ typedef void (^FBDialogAppCallCompletionHandler)( @see canPresentMessageDialogWithOpenGraphActionParams: @see canPresentMessageDialogWithParams: - @see canPresentMessageDialogWithPhotos: + @see canPresentMessageDialogWithPhotos */ + (BOOL)canPresentMessageDialog; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBErrorUtility.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBErrorUtility.h index 76225ecf9..3f346a8b9 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBErrorUtility.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBErrorUtility.h @@ -35,7 +35,7 @@ The error category can be used to understand the class of error received from Facebook. For more infomation on this see https://developers.facebook.com/docs/reference/api/errors/ - @param error the error to be categorized. + @param error The error to be categorized. */ + (FBErrorCategory)errorCategoryForError:(NSError *)error; @@ -49,7 +49,7 @@ take an action before the application continues to attempt a Facebook connection. For more infomation on this see https://developers.facebook.com/docs/reference/api/errors/ - @param error the error to inspect. + @param error The error to inspect. */ + (BOOL)shouldNotifyUserForError:(NSError *)error; @@ -59,7 +59,7 @@ Not all Facebook errors yield a message suitable for user display; however in all cases where +shouldNotifyUserForError: returns YES, this method returns a localizable message suitable for display. - @param error the error to inspect. + @param error The error to inspect. */ + (NSString *)userMessageForError:(NSError *)error; @@ -69,7 +69,7 @@ Not all Facebook errors yield a localized message/title suitable for user display; however in all cases when title is available, user should be notified. - @param error the error to inspect. + @param error The error to inspect. */ + (NSString *)userTitleForError:(NSError *)error; @@ -77,6 +77,8 @@ @abstract YES if given error is transient and may succeed if the initial action is retried as-is. Application may use this information to display a "Retry" button, if user should be notified about this error. + + @param error The error to inspect. */ + (BOOL)isTransientError:(NSError *)error; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBFriendPickerViewController.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBFriendPickerViewController.h index 29ffb5fcc..b3e270442 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBFriendPickerViewController.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBFriendPickerViewController.h @@ -51,7 +51,7 @@ /*! @abstract - The list of friends that are currently selected in the veiw. + The list of friends that are currently selected in the view. The items in the array are objects. @discussion diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBInsights.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBInsights.h index 4b7432186..c178536e1 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBInsights.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBInsights.h @@ -26,8 +26,10 @@ */ __attribute__ ((deprecated("use FBAppEventsFlushBehavior instead"))) typedef NS_ENUM(NSUInteger, FBInsightsFlushBehavior) { - FBInsightsFlushBehaviorAuto __attribute__ ((deprecated("use FBAppEventsFlushBehaviorAuto instead"))), - FBInsightsFlushBehaviorExplicitOnly __attribute__ ((deprecated("use FBAppEventsFlushBehaviorExplicitOnly instead"))), + /*! @deprecated use FBAppEventsFlushBehaviorAuto instead */ + FBInsightsFlushBehaviorAuto = 0, + /*! @deprecated use FBAppEventsFlushBehaviorExplicitOnly instead */ + FBInsightsFlushBehaviorExplicitOnly }; FBSDK_EXTERN NSString *const FBInsightsLoggingResultNotification __attribute__((deprecated)); @@ -40,19 +42,62 @@ FBSDK_EXTERN NSString *const FBInsightsLoggingResultNotification __attribute__(( __attribute__ ((deprecated("Use the FBAppEvents class instead"))) @interface FBInsights : NSObject +/*! + @abstract deprecated +*/ + (NSString *)appVersion __attribute__((deprecated)); +/*! + @abstract deprecated + @param appVersion deprecated +*/ + (void)setAppVersion:(NSString *)appVersion __attribute__((deprecated("use [FBSettings setAppVersion] instead"))); +/*! + @abstract deprecated + @param purchaseAmount deprecated + @param currency deprecated + */ + (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency __attribute__((deprecated("use [FBAppEvents logPurchase] instead"))); +/*! + @abstract deprecated + @param purchaseAmount deprecated + @param currency deprecated + @param parameters deprecated + */ + (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency parameters:(NSDictionary *)parameters __attribute__((deprecated("use [FBAppEvents logPurchase] instead"))); +/*! + @abstract deprecated + @param purchaseAmount deprecated + @param currency deprecated + @param parameters deprecated + @param session deprecated + */ + (void)logPurchase:(double)purchaseAmount currency:(NSString *)currency parameters:(NSDictionary *)parameters session:(FBSession *)session __attribute__((deprecated("use [FBAppEvents logPurchase] instead"))); - +/*! + @abstract deprecated + @param pixelID deprecated + @param value deprecated + */ + (void)logConversionPixel:(NSString *)pixelID valueOfPixel:(double)value __attribute__((deprecated)); +/*! + @abstract deprecated + @param pixelID deprecated + @param value deprecated + @param session deprecated + */ + (void)logConversionPixel:(NSString *)pixelID valueOfPixel:(double)value session:(FBSession *)session __attribute__((deprecated)); - +/*! + @abstract deprecated + */ + (FBInsightsFlushBehavior)flushBehavior __attribute__((deprecated("use [FBAppEvents flushBehavior] instead"))); +/*! + @abstract deprecated + @param flushBehavior deprecated + */ + (void)setFlushBehavior:(FBInsightsFlushBehavior)flushBehavior __attribute__((deprecated("use [FBAppEvents setFlushBehavior] instead"))); - +/*! + @abstract deprecated + */ + (void)flush __attribute__((deprecated("use [FBAppEvents flush] instead"))); @end diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBLikeControl.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBLikeControl.h index ad9e3a92e..511bf3b70 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBLikeControl.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBLikeControl.h @@ -59,6 +59,26 @@ typedef NS_ENUM(NSUInteger, FBLikeControlHorizontalAlignment) */ FBSDK_EXTERN NSString *NSStringFromFBLikeControlHorizontalAlignment(FBLikeControlHorizontalAlignment horizontalAlignment); +/*! + @typedef NS_ENUM (NSUInteger, FBLikeControlObjectType) + + @abstract Specifies the type of object referenced by the objectID of a like control. + */ +typedef NS_ENUM(NSUInteger, FBLikeControlObjectType) +{ + /*! The objectID refers to an unknown object type. */ + FBLikeControlObjectTypeUnknown = 0, + /*! The objectID refers to an Open Graph object. */ + FBLikeControlObjectTypeOpenGraphObject, + /*! The objectID refers to a Page object. */ + FBLikeControlObjectTypePage, +}; + +/*! + @abstract Converts an FBLikeControlObjectType to an NSString. + */ +FBSDK_EXTERN NSString *NSStringFromFBLikeControlObjectType(FBLikeControlObjectType objectType); + /*! @typedef NS_ENUM (NSUInteger, FBLikeControlStyle) @@ -124,6 +144,15 @@ FBSDK_EXTERN NSString *NSStringFromFBLikeControlStyle(FBLikeControlStyle style); */ @property (nonatomic, copy) NSString *objectID; +/*! + @abstract The type of object referenced by the objectID. + + @discussion If the objectType is unknown, the control will determine the objectType by querying the server with the + objectID. Specifying a value for the objectType is an optimization that should be used if the type is known by the + consumer. Consider setting the objectType if you know your objectType when setting the objectID. + */ +@property (nonatomic, assign) FBLikeControlObjectType objectType; + /*! @abstract The preferred maximum width (in points) for autolayout. diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBNativeDialogs.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBNativeDialogs.h index d1c86b530..ddf31a102 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBNativeDialogs.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBNativeDialogs.h @@ -66,6 +66,11 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `presentOSIntegratedShareDialogModallyFrom`. + @param viewController deprecated + @param initialText deprecated + @param image deprecated + @param url deprecated + @param handler deprecated */ + (BOOL)presentShareDialogModallyFrom:(UIViewController *)viewController initialText:(NSString *)initialText @@ -78,6 +83,11 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `presentOSIntegratedShareDialogModallyFrom`. + @param viewController deprecated + @param initialText deprecated + @param images deprecated + @param urls deprecated + @param handler deprecated */ + (BOOL)presentShareDialogModallyFrom:(UIViewController *)viewController initialText:(NSString *)initialText @@ -90,6 +100,12 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `presentOSIntegratedShareDialogModallyFrom`. + @param viewController deprecated + @param session deprecated + @param initialText deprecated + @param images deprecated + @param urls deprecated + @param handler deprecated */ + (BOOL)presentShareDialogModallyFrom:(UIViewController *)viewController session:(FBSession *)session @@ -103,6 +119,7 @@ __attribute__((deprecated)); @abstract Please note that this method has been deprecated, please migrate your code to use `FBDialogs` and the related method `canPresentOSIntegratedShareDialogWithSession`. + @param session deprecated */ + (BOOL)canPresentShareDialogWithSession:(FBSession *)session __attribute__((deprecated)); diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBRequestConnection.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBRequestConnection.h index 2f7cbae47..c5ef53752 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBRequestConnection.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBRequestConnection.h @@ -313,6 +313,10 @@ totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite; */ @property (nonatomic, assign) FBRequestConnectionErrorBehavior errorBehavior; +/*! + @abstract + The delegate object that receives updates. + */ @property (nonatomic, assign) id delegate; /*! diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBSettings.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBSettings.h index 242f49e1b..b2628f75e 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBSettings.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBSettings.h @@ -47,6 +47,9 @@ FBSDK_EXTERN NSString *const FBLoggingBehaviorInformational; /*! Log cache errors. */ FBSDK_EXTERN NSString *const FBLoggingBehaviorCacheErrors; +/*! Log errors from SDK UI controls */ +FBSDK_EXTERN NSString *const FBLoggingBehaviorUIControlErrors; + /*! Log errors likely to be preventable by the developer. This is in the default set of enabled logging behaviors. */ FBSDK_EXTERN NSString *const FBLoggingBehaviorDeveloperErrors; @@ -58,13 +61,13 @@ FBSDK_EXTERN NSString *const FBLoggingBehaviorDeveloperErrors; in release builds. */ typedef NS_ENUM(NSUInteger, FBBetaFeatures) { + /*! Default value indicating no beta features */ FBBetaFeaturesNone = 0, -#if defined(DEBUG) || defined(FB_BUILD_ONLY) -#endif }; /*! @typedef + @abstract Indicates if this app should be restricted */ typedef NS_ENUM(NSUInteger, FBRestrictedTreatment) { @@ -122,6 +125,7 @@ typedef NS_ENUM(NSUInteger, FBRestrictedTreatment) { @abstract This method is deprecated -- App Events favors using bundle identifiers to this. + @param appVersion deprecated */ + (void)setAppVersion:(NSString *)appVersion __attribute__ ((deprecated("App Events favors use of bundle identifiers for version identification."))); @@ -293,14 +297,18 @@ typedef NS_ENUM(NSUInteger, FBRestrictedTreatment) { /*! @method + @abstract Returns YES if the legacy Graph API mode is enabled */ + (BOOL)isPlatformCompatibilityEnabled; /*! @method + @abstract Configures the SDK to use the legacy platform. + @param enable indicates whether to use the legacy mode + @discussion Setting this flag has several effects: - FBRequests will target v1.0 of the Graph API. - Login will use the prior behavior without abilities to decline permission. diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTestSession.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTestSession.h index 73b5ec070..41fd0e200 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTestSession.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTestSession.h @@ -79,8 +79,12 @@ FBSDK_EXTERN NSString *kThirdTestUserTag; @property (readonly, copy) NSString *testAppID; /// The App Secret of the test app as configured in the plist. @property (readonly, copy) NSString *testAppSecret; -// Defaults to NO. If set to YES, reauthorize calls will fail with a nil token -// as if the user had cancelled it reauthorize. +/*! + @abstract + @discussion + Defaults to NO. If set to YES, reauthorize calls will fail with a nil token + as if the user had cancelled it reauthorize. +*/ @property (assign) BOOL disableReauthorize; /*! diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTooltipView.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTooltipView.h index 0dcae1b2f..4a92bf98e 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTooltipView.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FBTooltipView.h @@ -56,6 +56,7 @@ typedef NS_ENUM(NSUInteger, FBTooltipColorStyle) { /*! @abstract Gets or sets the amount of time in seconds the tooltip should be displayed. + @discussion Set this to zero to make the display permanent until explicitly dismissed. Defaults to six seconds. */ @@ -63,6 +64,7 @@ typedef NS_ENUM(NSUInteger, FBTooltipColorStyle) { /*! @abstract Gets or sets the color style after initialization. + @discussion Defaults to value passed to -initWithTagline:message:colorStyle:. */ @property (nonatomic, assign) FBTooltipColorStyle colorStyle; diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FacebookSDK.h b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FacebookSDK.h index 60cf4c3b5..12928c8cc 100644 --- a/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FacebookSDK.h +++ b/platforms/ios/FacebookSDK.framework/Versions/A/Headers/FacebookSDK.h @@ -138,6 +138,6 @@ */ -#define FB_IOS_SDK_VERSION_STRING @"3.19" -#define FB_IOS_SDK_TARGET_PLATFORM_VERSION @"v2.1" +#define FB_IOS_SDK_VERSION_STRING @"3.20.0" +#define FB_IOS_SDK_TARGET_PLATFORM_VERSION @"v2.2" diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png index be1dccd25..9bafbb3ef 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png and b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo.png differ diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png index 4b03929cb..3c1028adf 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png and b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/facebook-logo@2x.png differ diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png index 892419f35..ba8610000 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png and b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal.png differ diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png index daa4ba694..5362071fe 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png and b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-normal@2x.png differ diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png index 3f862c850..007388a0c 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png and b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed.png differ diff --git a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png index 7866e3da7..63a271b56 100644 Binary files a/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png and b/platforms/ios/FacebookSDK.framework/Versions/A/Resources/FBUserSettingsViewResources.bundle/images/silver-button-pressed@2x.png differ diff --git a/platforms/ios/HelloCordova/Plugins/com.phonegap.plugins.facebookconnect/FacebookConnectPlugin.m b/platforms/ios/HelloCordova/Plugins/com.phonegap.plugins.facebookconnect/FacebookConnectPlugin.m index 8bc68cbe0..af5f1523e 100644 --- a/platforms/ios/HelloCordova/Plugins/com.phonegap.plugins.facebookconnect/FacebookConnectPlugin.m +++ b/platforms/ios/HelloCordova/Plugins/com.phonegap.plugins.facebookconnect/FacebookConnectPlugin.m @@ -252,14 +252,15 @@ - (void)login:(CDVInvokedUrlCommand *)command { BOOL permissionsAllowed = YES; NSString *permissionsErrorMessage = @""; NSArray *permissions = nil; + CDVPluginResult *pluginResult; if ([command.arguments count] > 0) { permissions = command.arguments; } if (permissions == nil) { // We need permissions permissionsErrorMessage = @"No permissions specified at login"; - CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR - messageAsString:permissionsErrorMessage]; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsString:permissionsErrorMessage]; [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; return; } @@ -334,11 +335,9 @@ - (void)login:(CDVInvokedUrlCommand *)command { } if (!permissionsAllowed) { - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR - messageAsString:permissionsErrorMessage]; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR + messageAsString:permissionsErrorMessage]; [self.commandDelegate sendPluginResult:pluginResult callbackId:self.loginCallbackId]; - } else { - [super writeJavascript:nil]; } } @@ -415,7 +414,6 @@ - (void) showDialog:(CDVInvokedUrlCommand*)command } [self.commandDelegate sendPluginResult:pluginResult callbackId:self.dialogCallbackId]; }]; - [super writeJavascript:nil]; } // For optional ARC support diff --git a/plugin.xml b/plugin.xml index 0218daa7f..6200ff30c 100644 --- a/plugin.xml +++ b/plugin.xml @@ -2,7 +2,7 @@ + version="0.10.0"> Facebook Connect diff --git a/www/js/facebookConnectPlugin.js b/www/js/facebookConnectPlugin.js index b29cce4aa..d1516ac80 100644 --- a/www/js/facebookConnectPlugin.js +++ b/www/js/facebookConnectPlugin.js @@ -52,17 +52,7 @@ if (!window.cordova) { // Try will catch errors when SDK has not been init try { - FB.ui({ - method: options.method, - message: options.message, - name: options.name, - caption: options.caption, - description: ( - options.description - ), - href: options.href, - picture: options.picture - }, + FB.ui(options, function (response) { if (response && (response.request || !response.error_code)) { s(response); @@ -164,7 +154,7 @@ if (!window.cordova) { cookie : true, xfbml : true, version : version - }) + }); } };