- * Bundle parameters = new Bundle();
- * parameters.putString("method", "auth.expireSession");
- * String response = request(parameters);
- *
- *
- * @param parameters
- * Key-value pairs of parameters to the request. Refer to the
- * documentation: one of the parameters must be "method".
- * @throws IOException
- * if a network error occurs
- * @throws MalformedURLException
- * if accessing an invalid endpoint
- * @throws IllegalArgumentException
- * if one of the parameters is not "method"
- * @return JSON string representation of the response
- */
- public String request(Bundle parameters)
- throws MalformedURLException, IOException {
- if (!parameters.containsKey("method")) {
- throw new IllegalArgumentException("API method must be specified. "
- + "(parameters must contain key \"method\" and value). See"
- + " http://developers.facebook.com/docs/reference/rest/");
- }
- return request(null, parameters, "GET");
- }
-
- /**
- * Make a request to the Facebook Graph API without any parameters.
- *
- * See http://developers.facebook.com/docs/api
- *
- * Note that this method blocks waiting for a network response, so do not
- * call it in a UI thread.
- *
- * @param graphPath
- * Path to resource in the Facebook graph, e.g., to fetch data
- * about the currently logged authenticated user, provide "me",
- * which will fetch http://graph.facebook.com/me
- * @throws IOException
- * @throws MalformedURLException
- * @return JSON string representation of the response
- */
- public String request(String graphPath)
- throws MalformedURLException, IOException {
- return request(graphPath, new Bundle(), "GET");
- }
-
- /**
- * Make a request to the Facebook Graph API with the given string parameters
- * using an HTTP GET (default method).
- *
- * See http://developers.facebook.com/docs/api
- *
- * Note that this method blocks waiting for a network response, so do not
- * call it in a UI thread.
- *
- * @param graphPath
- * Path to resource in the Facebook graph, e.g., to fetch data
- * about the currently logged authenticated user, provide "me",
- * which will fetch http://graph.facebook.com/me
- * @param parameters
- * key-value string parameters, e.g. the path "search" with
- * parameters "q" : "facebook" would produce a query for the
- * following graph resource:
- * https://graph.facebook.com/search?q=facebook
- * @throws IOException
- * @throws MalformedURLException
- * @return JSON string representation of the response
- */
- public String request(String graphPath, Bundle parameters)
- throws MalformedURLException, IOException {
- return request(graphPath, parameters, "GET");
- }
-
- /**
- * Synchronously make a request to the Facebook Graph API with the given
- * HTTP method and string parameters. Note that binary data parameters
- * (e.g. pictures) are not yet supported by this helper function.
- *
- * See http://developers.facebook.com/docs/api
- *
- * Note that this method blocks waiting for a network response, so do not
- * call it in a UI thread.
- *
- * @param graphPath
- * Path to resource in the Facebook graph, e.g., to fetch data
- * about the currently logged authenticated user, provide "me",
- * which will fetch http://graph.facebook.com/me
- * @param params
- * Key-value string parameters, e.g. the path "search" with
- * parameters {"q" : "facebook"} would produce a query for the
- * following graph resource:
- * https://graph.facebook.com/search?q=facebook
- * @param httpMethod
- * http verb, e.g. "GET", "POST", "DELETE"
- * @throws IOException
- * @throws MalformedURLException
- * @return JSON string representation of the response
- */
- public String request(String graphPath, Bundle params, String httpMethod)
- throws FileNotFoundException, MalformedURLException, IOException {
- params.putString("format", "json");
- if (isSessionValid()) {
- params.putString(TOKEN, getAccessToken());
- }
- String url = (graphPath != null) ? GRAPH_BASE_URL + graphPath
- : RESTSERVER_URL;
- return Util.openUrl(url, httpMethod, params);
- }
-
- /**
- * Generate a UI dialog for the request action in the given Android context.
- *
- * Note that this method is asynchronous and the callback will be invoked in
- * the original calling thread (not in a background thread).
- *
- * @param context
- * The Android context in which we will generate this dialog.
- * @param action
- * String representation of the desired method: e.g. "login",
- * "stream.publish", ...
- * @param listener
- * Callback interface to notify the application when the dialog
- * has completed.
- */
- public void dialog(Context context, String action,
- DialogListener listener) {
- dialog(context, action, new Bundle(), listener);
- }
-
- /**
- * Generate a UI dialog for the request action in the given Android context
- * with the provided parameters.
- *
- * Note that this method is asynchronous and the callback will be invoked in
- * the original calling thread (not in a background thread).
- *
- * @param context
- * The Android context in which we will generate this dialog.
- * @param action
- * String representation of the desired method: e.g. "feed" ...
- * @param parameters
- * String key-value pairs to be passed as URL parameters.
- * @param listener
- * Callback interface to notify the application when the dialog
- * has completed.
- */
- public void dialog(Context context, String action, Bundle parameters,
- final DialogListener listener) {
-
- String endpoint = DIALOG_BASE_URL + action;
- parameters.putString("display", "touch");
- parameters.putString("redirect_uri", REDIRECT_URI);
-
- if (action.equals(LOGIN)) {
- parameters.putString("type", "user_agent");
- parameters.putString("client_id", mAppId);
- } else {
- parameters.putString("app_id", mAppId);
- }
-
- if (isSessionValid()) {
- parameters.putString(TOKEN, getAccessToken());
- }
- String url = endpoint + "?" + Util.encodeUrl(parameters);
- if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET)
- != PackageManager.PERMISSION_GRANTED) {
- Util.showAlert(context, "Error",
- "Application requires permission to access the Internet");
- } else {
- new FbDialog(context, url, listener).show();
- }
- }
-
- /**
- * @return boolean - whether this object has an non-expired session token
- */
- public boolean isSessionValid() {
- return (getAccessToken() != null) &&
- ((getAccessExpires() == 0) ||
- (System.currentTimeMillis() < getAccessExpires()));
- }
-
- /**
- * Retrieve the OAuth 2.0 access token for API access: treat with care.
- * Returns null if no session exists.
- *
- * @return String - access token
- */
- public String getAccessToken() {
- return mAccessToken;
- }
-
- /**
- * Retrieve the current session's expiration time (in milliseconds since
- * Unix epoch), or 0 if the session doesn't expire or doesn't exist.
- *
- * @return long - session expiration time
- */
- public long getAccessExpires() {
- return mAccessExpires;
- }
-
- /**
- * Retrieve the last time the token was updated (in milliseconds since
- * the Unix epoch), or 0 if the token has not been set.
- *
- * @return long - timestamp of the last token update.
- */
- public long getLastAccessUpdate() {
- return mLastAccessUpdate;
- }
-
- /**
- * Restore the token, expiration time, and last update time from cached values.
- * These should be values obtained from getAccessToken(), getAccessExpires, and
- * getLastAccessUpdate() respectively.
- *
- * @param accessToken - access token
- * @param accessExpires - access token expiration time
- * @param lastAccessUpdate - timestamp of the last token update
- */
- public void setTokenFromCache(String accessToken, long accessExpires, long lastAccessUpdate) {
- mAccessToken = accessToken;
- mAccessExpires = accessExpires;
- mLastAccessUpdate = lastAccessUpdate;
- }
-
- /**
- * Set the OAuth 2.0 access token for API access.
- *
- * @param token - access token
- */
- public void setAccessToken(String token) {
- mAccessToken = token;
- mLastAccessUpdate = System.currentTimeMillis();
- }
-
- /**
- * Set the current session's expiration time (in milliseconds since Unix
- * epoch), or 0 if the session doesn't expire.
- *
- * @param time - timestamp in milliseconds
- */
- public void setAccessExpires(long time) {
- mAccessExpires = time;
- }
-
- /**
- * Set the current session's duration (in seconds since Unix epoch), or "0"
- * if session doesn't expire.
- *
- * @param expiresIn
- * - duration in seconds (or 0 if the session doesn't expire)
- */
- public void setAccessExpiresIn(String expiresIn) {
- if (expiresIn != null) {
- long expires = expiresIn.equals("0")
- ? 0
- : System.currentTimeMillis() + Long.parseLong(expiresIn) * 1000L;
- setAccessExpires(expires);
- }
- }
-
- public String getAppId() {
- return mAppId;
- }
-
- public void setAppId(String appId) {
- mAppId = appId;
- }
-
- /**
- * Get Attribution ID for app install conversion tracking.
- * @param contentResolver
- * @return Attribution ID that will be used for conversion tracking. It will be null only if
- * the user has not installed or logged in to the Facebook app.
- */
- public static String getAttributionId(ContentResolver contentResolver) {
- String [] projection = {ATTRIBUTION_ID_COLUMN_NAME};
- Cursor c = contentResolver.query(ATTRIBUTION_ID_CONTENT_URI, projection, null, null, null);
- if (c == null || !c.moveToFirst()) {
- return null;
- }
- String attributionId = c.getString(c.getColumnIndex(ATTRIBUTION_ID_COLUMN_NAME));
-
- return attributionId;
- }
-
- /**
- * Get the auto install publish setting. If true, an install event will be published during authorize(), unless
- * it has occurred previously or the app does not have install attribution enabled on the application's developer
- * config page.
- * @return
- */
- public boolean getShouldAutoPublishInstall() {
- return shouldAutoPublishInstall;
- }
-
- /**
- * Sets whether auto publishing of installs will occur.
- * @param value
- */
- public void setShouldAutoPublishInstall(boolean value) {
- shouldAutoPublishInstall = value;
- }
-
- /**
- * Manually publish install attribution to the facebook graph. Internally handles tracking repeat calls to prevent
- * multiple installs being published to the graph.
- * @param context
- * @return returns false on error. Applications should retry until true is returned. Safe to call again after
- * true is returned.
- */
- public boolean publishInstall(final Context context) {
- try {
- // copy the application id to guarantee thread safety..
- String applicationId = mAppId;
- if (applicationId != null) {
- publishInstall(this, applicationId, context);
- return true;
- }
- } catch (Exception e) {
- // if there was an error, fall through to the failure case.
- Util.logd("Facebook-publish", e.getMessage());
- }
- return false;
- }
-
- /**
- * This function does the heavy lifting of publishing an install.
- * @param fb
- * @param applicationId
- * @param context
- * @throws Exception
- */
- private static void publishInstall(final Facebook fb, final String applicationId, final Context context)
- throws JSONException, FacebookError, MalformedURLException, IOException {
-
- String attributionId = Facebook.getAttributionId(context.getContentResolver());
- SharedPreferences preferences = context.getSharedPreferences(ATTRIBUTION_PREFERENCES, Context.MODE_PRIVATE);
- String pingKey = applicationId+"ping";
- long lastPing = preferences.getLong(pingKey, 0);
- if (lastPing == 0 && attributionId != null) {
- Bundle supportsAttributionParams = new Bundle();
- supportsAttributionParams.putString(APPLICATION_FIELDS, SUPPORTS_ATTRIBUTION);
- JSONObject supportResponse = Util.parseJson(fb.request(applicationId, supportsAttributionParams));
- Object doesSupportAttribution = (Boolean)supportResponse.get(SUPPORTS_ATTRIBUTION);
-
- if (!(doesSupportAttribution instanceof Boolean)) {
- throw new JSONException(String.format(
- "%s contains %s instead of a Boolean", SUPPORTS_ATTRIBUTION, doesSupportAttribution));
- }
-
- if ((Boolean)doesSupportAttribution) {
- Bundle publishParams = new Bundle();
- publishParams.putString(ANALYTICS_EVENT, MOBILE_INSTALL_EVENT);
- publishParams.putString(ATTRIBUTION_KEY, attributionId);
-
- String publishUrl = String.format(PUBLISH_ACTIVITY_PATH, applicationId);
-
- fb.request(publishUrl, publishParams, "POST");
-
- // denote success since no error threw from the post.
- SharedPreferences.Editor editor = preferences.edit();
- editor.putLong(pingKey, System.currentTimeMillis());
- editor.commit();
- }
- }
- }
-
- void autoPublishAsync(final Context context) {
- AutoPublishAsyncTask asyncTask = null;
- synchronized (this) {
- if (mAutoPublishAsyncTask == null && getShouldAutoPublishInstall()) {
- // copy the application id to guarantee thread safety against our container.
- String applicationId = Facebook.this.mAppId;
-
- // skip publish if we don't have an application id.
- if (applicationId != null) {
- asyncTask = mAutoPublishAsyncTask = new AutoPublishAsyncTask(applicationId, context);
- }
- }
- }
-
- if (asyncTask != null) {
- asyncTask.execute();
- }
- }
-
- /**
- * Async implementation to allow auto publishing to not block the ui thread.
- */
- private class AutoPublishAsyncTask extends AsyncTask+ * The AppEventsLogger class allows the developer to log various types of events back to Facebook. In order to log + * events, the app must create an instance of this class via a {@link #newLogger newLogger} method, and then call + * the various "log" methods off of that. + *
+ *+ * This client-side event logging is then available through Facebook App Insights + * and for use with Facebook Ads conversion tracking and optimization. + *
+ *+ * The AppEventsLogger class has a few related roles: + *
+ * {@code + *+ * Do not start this activity directly. + */ +public class LoginActivity extends Activity { + static final String RESULT_KEY = "com.facebook.LoginActivity:Result"; + + private static final String TAG = LoginActivity.class.getName(); + private static final String NULL_CALLING_PKG_ERROR_MSG = + "Cannot call LoginActivity with a null calling package. " + + "This can occur if the launchMode of the caller is singleInstance."; + private static final String SAVED_CALLING_PKG_KEY = "callingPackage"; + private static final String SAVED_AUTH_CLIENT = "authorizationClient"; + private static final String EXTRA_REQUEST = "request"; + + private String callingPackage; + private AuthorizationClient authorizationClient; + private AuthorizationClient.AuthorizationRequest request; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + int id = this.getResources().getIdentifier("com_facebook_login_activity_layout", "layout", this.getPackageName()); + setContentView(id); + + if (savedInstanceState != null) { + callingPackage = savedInstanceState.getString(SAVED_CALLING_PKG_KEY); + authorizationClient = (AuthorizationClient) savedInstanceState.getSerializable(SAVED_AUTH_CLIENT); + } else { + callingPackage = getCallingPackage(); + authorizationClient = new AuthorizationClient(); + request = (AuthorizationClient.AuthorizationRequest) getIntent().getSerializableExtra(EXTRA_REQUEST); + } + + authorizationClient.setContext(this); + authorizationClient.setOnCompletedListener(new AuthorizationClient.OnCompletedListener() { + @Override + public void onCompleted(AuthorizationClient.Result outcome) { + onAuthClientCompleted(outcome); + } + }); + authorizationClient.setBackgroundProcessingListener(new AuthorizationClient.BackgroundProcessingListener() { + @Override + public void onBackgroundProcessingStarted() { + findViewById(R.id.com_facebook_login_activity_progress_bar).setVisibility(View.VISIBLE); + } + + @Override + public void onBackgroundProcessingStopped() { + findViewById(R.id.com_facebook_login_activity_progress_bar).setVisibility(View.GONE); + } + }); + } + + private void onAuthClientCompleted(AuthorizationClient.Result outcome) { + request = null; + + int resultCode = (outcome.code == AuthorizationClient.Result.Code.CANCEL) ? + RESULT_CANCELED : RESULT_OK; + + Bundle bundle = new Bundle(); + bundle.putSerializable(RESULT_KEY, outcome); + + Intent resultIntent = new Intent(); + resultIntent.putExtras(bundle); + setResult(resultCode, resultIntent); + + finish(); + } + + @Override + public void onResume() { + super.onResume(); + + // If the calling package is null, this generally means that the callee was started + // with a launchMode of singleInstance. Unfortunately, Android does not allow a result + // to be set when the callee is a singleInstance, so we log an error and return. + if (callingPackage == null) { + Log.e(TAG, NULL_CALLING_PKG_ERROR_MSG); + finish(); + return; + } + + authorizationClient.startOrContinueAuth(request); + } + + @Override + public void onPause() { + super.onPause(); + + authorizationClient.cancelCurrentHandler(); + findViewById(R.id.com_facebook_login_activity_progress_bar).setVisibility(View.GONE); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(SAVED_CALLING_PKG_KEY, callingPackage); + outState.putSerializable(SAVED_AUTH_CLIENT, authorizationClient); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + authorizationClient.onActivityResult(requestCode, resultCode, data); + } + + static Bundle populateIntentExtras(AuthorizationClient.AuthorizationRequest request) { + Bundle extras = new Bundle(); + extras.putSerializable(EXTRA_REQUEST, request); + return extras; + } +} diff --git a/src/android/facebook/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java b/src/android/facebook/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java new file mode 100644 index 000000000..dc4108656 --- /dev/null +++ b/src/android/facebook/FacebookLib/src/com/facebook/NativeAppCallAttachmentStore.java @@ -0,0 +1,225 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.Log; +import com.facebook.internal.Utility; +import com.facebook.internal.Validate; + +import java.io.*; +import java.net.URLEncoder; +import java.util.*; + +/** + *+ * } + *
This class works in conjunction with {@link NativeAppCallContentProvider} to allow apps to attach binary + * attachments (e.g., images) to native dialogs launched via the {@link com.facebook.widget.FacebookDialog} + * class. It stores attachments in temporary files and allows the Facebook application to retrieve them via + * the content provider.
+ * + *Callers are generally not expected to need to use this class directly; + * see {@link com.facebook.widget.FacebookDialog.OpenGraphActionDialogBuilder#setImageAttachmentsForObject(String, + * java.util.List) OpenGraphActionDialogBuilder.setImageAttachmentsForObject} for an example of a function + * that will accept attachments, attach them to the native dialog call, and add them to the content provider + * automatically.
+ **/ +public final class NativeAppCallAttachmentStore implements NativeAppCallContentProvider.AttachmentDataSource { + private static final String TAG = NativeAppCallAttachmentStore.class.getName(); + static final String ATTACHMENTS_DIR_NAME = "com.facebook.NativeAppCallAttachmentStore.files"; + private static File attachmentsDirectory; + + /** + * Adds a number of bitmap attachments associated with a native app call. The attachments will be + * served via {@link NativeAppCallContentProvider#openFile(android.net.Uri, String) openFile}. + * + * @param context the Context the call is being made from + * @param callId the unique ID of the call + * @param imageAttachments a Map of attachment names to Bitmaps; the attachment names will be part of + * the URI processed by openFile + * @throws java.io.IOException + */ + public void addAttachmentsForCall(Context context, UUID callId, MapImplements a + * ContentProvider that can be used to provide binary attachments (e.g., images) to calls made + * via @link FacebookDialog}. The {@link NativeAppCallAttachmentStore} class provides methods to attach + * and clean up the attachments. + * + *
Note that this ContentProvider is only necessary if an application wishes to attach images, etc., that are + * stored in memory and do not have another way to be referenced by a content URI. For images obtained from, + * e.g., the Camera or Gallery, that already have a content URI associated with them, use of this class is not + * necessary.
+ * + *If an application wishes to attach images that are stored in-memory within the application, this content
+ * provider must be listed in the application's AndroidManifest.xml, and it should be named according to the
+ * pattern "com.facebook.app.NativeAppCallContentProvider{FACEBOOK_APP_ID}"
. See the
+ * {@link NativeAppCallContentProvider#getAttachmentUrl(String) getContentProviderName} method.
+ * Session is used to authenticate a user and manage the user's session with + * Facebook. + *
+ *+ * Sessions must be opened before they can be used to make a Request. When a + * Session is created, it attempts to initialize itself from a TokenCachingStrategy. + * Closing the session can optionally clear this cache. The Session lifecycle + * uses {@link SessionState SessionState} to indicate its state. Once a Session has + * been closed, it can't be re-opened; a new Session must be created. + *
+ *+ * Instances of Session provide state change notification via a callback + * interface, {@link Session.StatusCallback StatusCallback}. + *
+ */ +public class Session implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The logging tag used by Session. + */ + public static final String TAG = Session.class.getCanonicalName(); + + /** + * The default activity code used for authorization. + * + * @see #openForRead(OpenRequest) + * open + */ + public static final int DEFAULT_AUTHORIZE_ACTIVITY_CODE = 0xface; + + /** + * If Session authorization fails and provides a web view error code, the + * web view error code is stored in the Bundle returned from + * {@link #getAuthorizationBundle getAuthorizationBundle} under this key. + */ + public static final String WEB_VIEW_ERROR_CODE_KEY = "com.facebook.sdk.WebViewErrorCode"; + + /** + * If Session authorization fails and provides a failing url, the failing + * url is stored in the Bundle returned from {@link #getAuthorizationBundle + * getAuthorizationBundle} under this key. + */ + public static final String WEB_VIEW_FAILING_URL_KEY = "com.facebook.sdk.FailingUrl"; + + /** + * The action used to indicate that the active session has been set. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_SET = "com.facebook.sdk.ACTIVE_SESSION_SET"; + + /** + * The action used to indicate that the active session has been set to null. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_UNSET = "com.facebook.sdk.ACTIVE_SESSION_UNSET"; + + /** + * The action used to indicate that the active session has been opened. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_OPENED = "com.facebook.sdk.ACTIVE_SESSION_OPENED"; + + /** + * The action used to indicate that the active session has been closed. This should + * be used as an action in an IntentFilter and BroadcastReceiver registered with + * the {@link android.support.v4.content.LocalBroadcastManager}. + */ + public static final String ACTION_ACTIVE_SESSION_CLOSED = "com.facebook.sdk.ACTIVE_SESSION_CLOSED"; + + /** + * Session takes application id as a constructor parameter. If this is null, + * Session will attempt to load the application id from + * application/meta-data using this String as the key. + */ + public static final String APPLICATION_ID_PROPERTY = "com.facebook.sdk.ApplicationId"; + + private static final Object STATIC_LOCK = new Object(); + private static Session activeSession; + private static volatile Context staticContext; + + // Token extension constants + private static final int TOKEN_EXTEND_THRESHOLD_SECONDS = 24 * 60 * 60; // 1 + // day + private static final int TOKEN_EXTEND_RETRY_SECONDS = 60 * 60; // 1 hour + + private static final String SESSION_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.saveSessionKey"; + private static final String AUTH_BUNDLE_SAVE_KEY = "com.facebook.sdk.Session.authBundleKey"; + private static final String PUBLISH_PERMISSION_PREFIX = "publish"; + private static final String MANAGE_PERMISSION_PREFIX = "manage"; + + @SuppressWarnings("serial") + private static final Set+ * Returns the Date at which the current token will expire. + *
+ *+ * Note that Session automatically attempts to extend the lifetime of Tokens + * as needed when Facebook requests are made. + *
+ * + * @return the Date at which the current token will expire, or null if there is no access token + */ + public final Date getExpirationDate() { + synchronized (this.lock) { + return (this.tokenInfo == null) ? null : this.tokenInfo.getExpires(); + } + } + + /** + *+ * Returns the list of permissions associated with the session. + *
+ *+ * If there is a valid token, this represents the permissions granted by + * that token. This can change during calls to + * {@link #requestNewReadPermissions} + * or {@link #requestNewPublishPermissions}. + *
+ * + * @return the list of permissions associated with the session, or null if there is no access token + */ + public final List+ * Logs a user in to Facebook. + *
+ *+ * A session may not be used with {@link Request Request} and other classes + * in the SDK until it is open. If, prior to calling open, the session is in + * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} + * state, and the requested permissions are a subset of the previously authorized + * permissions, then the Session becomes usable immediately with no user interaction. + *
+ *+ * The permissions associated with the openRequest passed to this method must + * be read permissions only (or null/empty). It is not allowed to pass publish + * permissions to this method and will result in an exception being thrown. + *
+ *+ * Any open method must be called at most once, and cannot be called after the + * Session is closed. Calling the method at an invalid time will result in + * UnsuportedOperationException. + *
+ * + * @param openRequest the open request, can be null only if the Session is in the + * {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state + * @throws FacebookException if any publish or manage permissions are requested + */ + public final void openForRead(OpenRequest openRequest) { + open(openRequest, SessionAuthorizationType.READ); + } + + /** + *+ * Logs a user in to Facebook. + *
+ *+ * A session may not be used with {@link Request Request} and other classes + * in the SDK until it is open. If, prior to calling open, the session is in + * the {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} + * state, and the requested permissions are a subset of the previously authorized + * permissions, then the Session becomes usable immediately with no user interaction. + *
+ *+ * The permissions associated with the openRequest passed to this method must + * be publish or manage permissions only and must be non-empty. Any read permissions + * will result in a warning, and may fail during server-side authorization. Also, an application + * must have at least basic read permissions prior to requesting publish permissions, so + * this method should only be used if the application knows that the user has already granted + * read permissions to the application; otherwise, openForRead should be used, followed by a + * call to requestNewPublishPermissions. For more information on this flow, see + * https://developers.facebook.com/docs/facebook-login/permissions/. + *
+ *+ * Any open method must be called at most once, and cannot be called after the + * Session is closed. Calling the method at an invalid time will result in + * UnsuportedOperationException. + *
+ * + * @param openRequest the open request, can be null only if the Session is in the + * {@link SessionState#CREATED_TOKEN_LOADED CREATED_TOKEN_LOADED} state + * @throws FacebookException if the passed in request is null or has no permissions set. + */ + public final void openForPublish(OpenRequest openRequest) { + open(openRequest, SessionAuthorizationType.PUBLISH); + } + + /** + * Opens a session based on an existing Facebook access token. This method should be used + * only in instances where an application has previously obtained an access token and wishes + * to import it into the Session/TokenCachingStrategy-based session-management system. An + * example would be an application which previously did not use the Facebook SDK for Android + * and implemented its own session-management scheme, but wishes to implement an upgrade path + * for existing users so they do not need to log in again when upgrading to a version of + * the app that uses the SDK. + * + * No validation is done that the token, token source, or permissions are actually valid. + * It is the caller's responsibility to ensure that these accurately reflect the state of + * the token that has been passed in, or calls to the Facebook API may fail. + * + * @param accessToken the access token obtained from Facebook + * @param callback a callback that will be called when the session status changes; may be null + */ + public final void open(AccessToken accessToken, StatusCallback callback) { + synchronized (this.lock) { + if (pendingAuthorizationRequest != null) { + throw new UnsupportedOperationException( + "Session: an attempt was made to open a session that has a pending request."); + } + + if (state.isClosed()) { + throw new UnsupportedOperationException( + "Session: an attempt was made to open a previously-closed session."); + } else if (state != SessionState.CREATED && state != SessionState.CREATED_TOKEN_LOADED) { + throw new UnsupportedOperationException( + "Session: an attempt was made to open an already opened session."); + } + + if (callback != null) { + addCallback(callback); + } + + this.tokenInfo = accessToken; + + if (this.tokenCachingStrategy != null) { + this.tokenCachingStrategy.save(accessToken.toCacheBundle()); + } + + final SessionState oldState = state; + state = SessionState.OPENED; + this.postStateChange(oldState, state, null); + } + + autoPublishAsync(); + } + + /** + *+ * Issues a request to add new read permissions to the Session. + *
+ *+ * If successful, this will update the set of permissions on this session to + * match the newPermissions. If this fails, the Session remains unchanged. + *
+ *+ * The permissions associated with the newPermissionsRequest passed to this method must + * be read permissions only (or null/empty). It is not allowed to pass publish + * permissions to this method and will result in an exception being thrown. + *
+ * + * @param newPermissionsRequest the new permissions request + */ + public final void requestNewReadPermissions(NewPermissionsRequest newPermissionsRequest) { + requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.READ); + } + + /** + *+ * Issues a request to add new publish or manage permissions to the Session. + *
+ *+ * If successful, this will update the set of permissions on this session to + * match the newPermissions. If this fails, the Session remains unchanged. + *
+ *+ * The permissions associated with the newPermissionsRequest passed to this method must + * be publish or manage permissions only and must be non-empty. Any read permissions + * will result in a warning, and may fail during server-side authorization. + *
+ * + * @param newPermissionsRequest the new permissions request + */ + public final void requestNewPublishPermissions(NewPermissionsRequest newPermissionsRequest) { + requestNewPermissions(newPermissionsRequest, SessionAuthorizationType.PUBLISH); + } + + /** + * Provides an implementation for {@link Activity#onActivityResult + * onActivityResult} that updates the Session based on information returned + * during the authorization flow. The Activity that calls open or + * requestNewPermissions should forward the resulting onActivityResult call here to + * update the Session state based on the contents of the resultCode and + * data. + * + * @param currentActivity The Activity that is forwarding the onActivityResult call. + * @param requestCode The requestCode parameter from the forwarded call. When this + * onActivityResult occurs as part of Facebook authorization + * flow, this value is the activityCode passed to open or + * authorize. + * @param resultCode An int containing the resultCode parameter from the forwarded + * call. + * @param data The Intent passed as the data parameter from the forwarded + * call. + * @return A boolean indicating whether the requestCode matched a pending + * authorization request for this Session. + */ + public final boolean onActivityResult(Activity currentActivity, int requestCode, int resultCode, Intent data) { + Validate.notNull(currentActivity, "currentActivity"); + + initializeStaticContext(currentActivity); + + synchronized (lock) { + if (pendingAuthorizationRequest == null || (requestCode != pendingAuthorizationRequest.getRequestCode())) { + return false; + } + } + + Exception exception = null; + AuthorizationClient.Result.Code code = AuthorizationClient.Result.Code.ERROR; + + if (data != null) { + AuthorizationClient.Result result = (AuthorizationClient.Result) data.getSerializableExtra( + LoginActivity.RESULT_KEY); + if (result != null) { + // This came from LoginActivity. + handleAuthorizationResult(resultCode, result); + return true; + } else if (authorizationClient != null) { + // Delegate to the auth client. + authorizationClient.onActivityResult(requestCode, resultCode, data); + return true; + } + } else if (resultCode == Activity.RESULT_CANCELED) { + exception = new FacebookOperationCanceledException("User canceled operation."); + code = AuthorizationClient.Result.Code.CANCEL; + } + + if (exception == null) { + exception = new FacebookException("Unexpected call to Session.onActivityResult"); + } + + logAuthorizationComplete(code, null, exception); + finishAuthOrReauth(null, exception); + + return true; + } + + /** + * Closes the local in-memory Session object, but does not clear the + * persisted token cache. + */ + public final void close() { + synchronized (this.lock) { + final SessionState oldState = this.state; + + switch (this.state) { + case CREATED: + case OPENING: + this.state = SessionState.CLOSED_LOGIN_FAILED; + postStateChange(oldState, this.state, new FacebookException( + "Log in attempt aborted.")); + break; + + case CREATED_TOKEN_LOADED: + case OPENED: + case OPENED_TOKEN_UPDATED: + this.state = SessionState.CLOSED; + postStateChange(oldState, this.state, null); + break; + + case CLOSED: + case CLOSED_LOGIN_FAILED: + break; + } + } + } + + /** + * Closes the local in-memory Session object and clears any persisted token + * cache related to the Session. + */ + public final void closeAndClearTokenInformation() { + if (this.tokenCachingStrategy != null) { + this.tokenCachingStrategy.clear(); + } + Utility.clearFacebookCookies(staticContext); + Utility.clearCaches(staticContext); + close(); + } + + /** + * Adds a callback that will be called when the state of this Session changes. + * + * @param callback the callback + */ + public final void addCallback(StatusCallback callback) { + synchronized (callbacks) { + if (callback != null && !callbacks.contains(callback)) { + callbacks.add(callback); + } + } + } + + /** + * Removes a StatusCallback from this Session. + * + * @param callback the callback + */ + public final void removeCallback(StatusCallback callback) { + synchronized (callbacks) { + callbacks.remove(callback); + } + } + + @Override + public String toString() { + return new StringBuilder().append("{Session").append(" state:").append(this.state).append(", token:") + .append((this.tokenInfo == null) ? "null" : this.tokenInfo).append(", appId:") + .append((this.applicationId == null) ? "null" : this.applicationId).append("}").toString(); + } + + void extendTokenCompleted(Bundle bundle) { + synchronized (this.lock) { + final SessionState oldState = this.state; + + switch (this.state) { + case OPENED: + this.state = SessionState.OPENED_TOKEN_UPDATED; + postStateChange(oldState, this.state, null); + break; + case OPENED_TOKEN_UPDATED: + break; + default: + // Silently ignore attempts to refresh token if we are not open + Log.d(TAG, "refreshToken ignored in state " + this.state); + return; + } + this.tokenInfo = AccessToken.createFromRefresh(this.tokenInfo, bundle); + if (this.tokenCachingStrategy != null) { + this.tokenCachingStrategy.save(this.tokenInfo.toCacheBundle()); + } + } + } + + private Object writeReplace() { + return new SerializationProxyV1(applicationId, state, tokenInfo, + lastAttemptedTokenExtendDate, false, pendingAuthorizationRequest); + } + + // have a readObject that throws to prevent spoofing + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + throw new InvalidObjectException("Cannot readObject, serialization proxy required"); + } + + /** + * Save the Session object into the supplied Bundle. This method is intended to be called from an + * Activity or Fragment's onSaveInstanceState method in order to preserve Sessions across Activity lifecycle events. + * + * @param session the Session to save + * @param bundle the Bundle to save the Session to + */ + public static final void saveSession(Session session, Bundle bundle) { + if (bundle != null && session != null && !bundle.containsKey(SESSION_BUNDLE_SAVE_KEY)) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try { + new ObjectOutputStream(outputStream).writeObject(session); + } catch (IOException e) { + throw new FacebookException("Unable to save session.", e); + } + bundle.putByteArray(SESSION_BUNDLE_SAVE_KEY, outputStream.toByteArray()); + bundle.putBundle(AUTH_BUNDLE_SAVE_KEY, session.authorizationBundle); + } + } + + /** + * Restores the saved session from a Bundle, if any. Returns the restored Session or + * null if it could not be restored. This method is intended to be called from an Activity or Fragment's + * onCreate method when a Session has previously been saved into a Bundle via saveState to preserve a Session + * across Activity lifecycle events. + * + * @param context the Activity or Service creating the Session, must not be null + * @param cachingStrategy the TokenCachingStrategy to use to load and store the token. If this is + * null, a default token cachingStrategy that stores data in + * SharedPreferences will be used + * @param callback the callback to notify for Session state changes, can be null + * @param bundle the bundle to restore the Session from + * @return the restored Session, or null + */ + public static final Session restoreSession( + Context context, TokenCachingStrategy cachingStrategy, StatusCallback callback, Bundle bundle) { + if (bundle == null) { + return null; + } + byte[] data = bundle.getByteArray(SESSION_BUNDLE_SAVE_KEY); + if (data != null) { + ByteArrayInputStream is = new ByteArrayInputStream(data); + try { + Session session = (Session) (new ObjectInputStream(is)).readObject(); + initializeStaticContext(context); + if (cachingStrategy != null) { + session.tokenCachingStrategy = cachingStrategy; + } else { + session.tokenCachingStrategy = new SharedPreferencesTokenCachingStrategy(context); + } + if (callback != null) { + session.addCallback(callback); + } + session.authorizationBundle = bundle.getBundle(AUTH_BUNDLE_SAVE_KEY); + return session; + } catch (ClassNotFoundException e) { + Log.w(TAG, "Unable to restore session", e); + } catch (IOException e) { + Log.w(TAG, "Unable to restore session.", e); + } + } + return null; + } + + + /** + * Returns the current active Session, or null if there is none. + * + * @return the current active Session, or null if there is none. + */ + public static final Session getActiveSession() { + synchronized (Session.STATIC_LOCK) { + return Session.activeSession; + } + } + + /** + *+ * Sets the current active Session. + *
+ *+ * The active Session is used implicitly by predefined Request factory + * methods as well as optionally by UI controls in the sdk. + *
+ *+ * It is legal to set this to null, or to a Session that is not yet open. + *
+ * + * @param session A Session to use as the active Session, or null to indicate + * that there is no active Session. + */ + public static final void setActiveSession(Session session) { + synchronized (Session.STATIC_LOCK) { + if (session != Session.activeSession) { + Session oldSession = Session.activeSession; + + if (oldSession != null) { + oldSession.close(); + } + + Session.activeSession = session; + + if (oldSession != null) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_UNSET); + } + + if (session != null) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_SET); + + if (session.isOpened()) { + postActiveSessionAction(Session.ACTION_ACTIVE_SESSION_OPENED); + } + } + } + } + } + + /** + * If a cached token is available, creates and opens the session and makes it active without any user interaction, + * otherwise this does nothing. + * + * @param context The Context creating this session + * @return The new session or null if one could not be created + */ + public static Session openActiveSessionFromCache(Context context) { + return openActiveSession(context, false, null); + } + + /** + * If allowLoginUI is true, this will create a new Session, make it active, and + * open it. If the default token cache is not available, then this will request + * basic permissions. If the default token cache is available and cached tokens + * are loaded, this will use the cached token and associated permissions. + * + * If allowedLoginUI is false, this will only create the active session and open + * it if it requires no user interaction (i.e. the token cache is available and + * there are cached tokens). + * + * @param activity The Activity that is opening the new Session. + * @param allowLoginUI if false, only sets the active session and opens it if it + * does not require user interaction + * @param callback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. May be null. + * @return The new Session or null if one could not be created + */ + public static Session openActiveSession(Activity activity, boolean allowLoginUI, + StatusCallback callback) { + return openActiveSession(activity, allowLoginUI, new OpenRequest(activity).setCallback(callback)); + } + + /** + * If allowLoginUI is true, this will create a new Session, make it active, and + * open it. If the default token cache is not available, then this will request + * basic permissions. If the default token cache is available and cached tokens + * are loaded, this will use the cached token and associated permissions. + * + * If allowedLoginUI is false, this will only create the active session and open + * it if it requires no user interaction (i.e. the token cache is available and + * there are cached tokens). + * + * @param context The Activity or Service creating this Session + * @param fragment The Fragment that is opening the new Session. + * @param allowLoginUI if false, only sets the active session and opens it if it + * does not require user interaction + * @param callback The {@link StatusCallback SessionStatusCallback} to + * notify regarding Session state changes. + * @return The new Session or null if one could not be created + */ + public static Session openActiveSession(Context context, Fragment fragment, + boolean allowLoginUI, StatusCallback callback) { + return openActiveSession(context, allowLoginUI, new OpenRequest(fragment).setCallback(callback)); + } + + /** + * Opens a session based on an existing Facebook access token, and also makes this session + * the currently active session. This method should be used + * only in instances where an application has previously obtained an access token and wishes + * to import it into the Session/TokenCachingStrategy-based session-management system. A primary + * example would be an application which previously did not use the Facebook SDK for Android + * and implemented its own session-management scheme, but wishes to implement an upgrade path + * for existing users so they do not need to log in again when upgrading to a version of + * the app that uses the SDK. In general, this method will be called only once, when the app + * detects that it has been upgraded -- after that, the usual Session lifecycle methods + * should be used to manage the session and its associated token. + * + * No validation is done that the token, token source, or permissions are actually valid. + * It is the caller's responsibility to ensure that these accurately reflect the state of + * the token that has been passed in, or calls to the Facebook API may fail. + * + * @param context the Context to use for creation the session + * @param accessToken the access token obtained from Facebook + * @param callback a callback that will be called when the session status changes; may be null + * @return The new Session or null if one could not be created + */ + public static Session openActiveSessionWithAccessToken(Context context, AccessToken accessToken, + StatusCallback callback) { + Session session = new Session(context, null, null, false); + + setActiveSession(session); + session.open(accessToken, callback); + + return session; + } + + private static Session openActiveSession(Context context, boolean allowLoginUI, OpenRequest openRequest) { + Session session = new Builder(context).build(); + if (SessionState.CREATED_TOKEN_LOADED.equals(session.getState()) || allowLoginUI) { + setActiveSession(session); + session.openForRead(openRequest); + return session; + } + return null; + } + + static Context getStaticContext() { + return staticContext; + } + + static void initializeStaticContext(Context currentContext) { + if ((currentContext != null) && (staticContext == null)) { + Context applicationContext = currentContext.getApplicationContext(); + staticContext = (applicationContext != null) ? applicationContext : currentContext; + } + } + + void authorize(AuthorizationRequest request) { + boolean started = false; + + request.setApplicationId(applicationId); + + autoPublishAsync(); + + logAuthorizationStart(); + + started = tryLoginActivity(request); + + pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LOGIN_ACTIVITY, + started ? AppEventsConstants.EVENT_PARAM_VALUE_YES : AppEventsConstants.EVENT_PARAM_VALUE_NO); + + if (!started && request.isLegacy) { + pendingAuthorizationRequest.loggingExtras.put(AuthorizationClient.EVENT_EXTRAS_TRY_LEGACY, + AppEventsConstants.EVENT_PARAM_VALUE_YES); + + tryLegacyAuth(request); + started = true; + } + + if (!started) { + synchronized (this.lock) { + final SessionState oldState = this.state; + + switch (this.state) { + case CLOSED: + case CLOSED_LOGIN_FAILED: + return; + + default: + this.state = SessionState.CLOSED_LOGIN_FAILED; + + Exception exception = new FacebookException( + "Log in attempt failed: LoginActivity could not be started, and not legacy request"); + logAuthorizationComplete(AuthorizationClient.Result.Code.ERROR, null, exception); + postStateChange(oldState, this.state, exception); + } + } + } + } + + private void open(OpenRequest openRequest, SessionAuthorizationType authType) { + validatePermissions(openRequest, authType); + validateLoginBehavior(openRequest); + + SessionState newState; + synchronized (this.lock) { + if (pendingAuthorizationRequest != null) { + postStateChange(state, state, new UnsupportedOperationException( + "Session: an attempt was made to open a session that has a pending request.")); + return; + } + final SessionState oldState = this.state; + + switch (this.state) { + case CREATED: + this.state = newState = SessionState.OPENING; + if (openRequest == null) { + throw new IllegalArgumentException("openRequest cannot be null when opening a new Session"); + } + pendingAuthorizationRequest = openRequest; + break; + case CREATED_TOKEN_LOADED: + if (openRequest != null && !Utility.isNullOrEmpty(openRequest.getPermissions())) { + if (!Utility.isSubset(openRequest.getPermissions(), getPermissions())) { + pendingAuthorizationRequest = openRequest; + } + } + if (pendingAuthorizationRequest == null) { + this.state = newState = SessionState.OPENED; + } else { + this.state = newState = SessionState.OPENING; + } + break; + default: + throw new UnsupportedOperationException( + "Session: an attempt was made to open an already opened session."); + } + if (openRequest != null) { + addCallback(openRequest.getCallback()); + } + this.postStateChange(oldState, newState, null); + } + + if (newState == SessionState.OPENING) { + authorize(openRequest); + } + } + + private void requestNewPermissions(NewPermissionsRequest newPermissionsRequest, SessionAuthorizationType authType) { + validatePermissions(newPermissionsRequest, authType); + validateLoginBehavior(newPermissionsRequest); + + if (newPermissionsRequest != null) { + synchronized (this.lock) { + if (pendingAuthorizationRequest != null) { + throw new UnsupportedOperationException( + "Session: an attempt was made to request new permissions for a session that has a pending request."); + } + if (state.isOpened()) { + pendingAuthorizationRequest = newPermissionsRequest; + } else if (state.isClosed()) { + throw new UnsupportedOperationException( + "Session: an attempt was made to request new permissions for a session that has been closed."); + } else { + throw new UnsupportedOperationException( + "Session: an attempt was made to request new permissions for a session that is not currently open."); + } + } + + newPermissionsRequest.setValidateSameFbidAsToken(getAccessToken()); + addCallback(newPermissionsRequest.getCallback()); + authorize(newPermissionsRequest); + } + } + + private void validateLoginBehavior(AuthorizationRequest request) { + if (request != null && !request.isLegacy) { + Intent intent = new Intent(); + intent.setClass(getStaticContext(), LoginActivity.class); + if (!resolveIntent(intent)) { + throw new FacebookException(String.format( + "Cannot use SessionLoginBehavior %s when %s is not declared as an activity in AndroidManifest.xml", + request.getLoginBehavior(), LoginActivity.class.getName())); + } + } + } + + private void validatePermissions(AuthorizationRequest request, SessionAuthorizationType authType) { + if (request == null || Utility.isNullOrEmpty(request.getPermissions())) { + if (SessionAuthorizationType.PUBLISH.equals(authType)) { + throw new FacebookException("Cannot request publish or manage authorization with no permissions."); + } + return; // nothing to check + } + for (String permission : request.getPermissions()) { + if (isPublishPermission(permission)) { + if (SessionAuthorizationType.READ.equals(authType)) { + throw new FacebookException( + String.format( + "Cannot pass a publish or manage permission (%s) to a request for read authorization", + permission)); + } + } else { + if (SessionAuthorizationType.PUBLISH.equals(authType)) { + Log.w(TAG, + String.format( + "Should not pass a read permission (%s) to a request for publish or manage authorization", + permission)); + } + } + } + } + + public static boolean isPublishPermission(String permission) { + return permission != null && + (permission.startsWith(PUBLISH_PERMISSION_PREFIX) || + permission.startsWith(MANAGE_PERMISSION_PREFIX) || + OTHER_PUBLISH_PERMISSIONS.contains(permission)); + + } + + private void handleAuthorizationResult(int resultCode, AuthorizationClient.Result result) { + AccessToken newToken = null; + Exception exception = null; + if (resultCode == Activity.RESULT_OK) { + if (result.code == AuthorizationClient.Result.Code.SUCCESS) { + newToken = result.token; + } else { + exception = new FacebookAuthorizationException(result.errorMessage); + } + } else if (resultCode == Activity.RESULT_CANCELED) { + exception = new FacebookOperationCanceledException(result.errorMessage); + } + + logAuthorizationComplete(result.code, result.loggingExtras, exception); + + authorizationClient = null; + finishAuthOrReauth(newToken, exception); + } + + private void logAuthorizationStart() { + Bundle bundle = AuthorizationClient.newAuthorizationLoggingBundle(pendingAuthorizationRequest.getAuthId()); + bundle.putLong(AuthorizationClient.EVENT_PARAM_TIMESTAMP, System.currentTimeMillis()); + + // Log what we already know about the call in start event + try { + JSONObject extras = new JSONObject(); + extras.put(AuthorizationClient.EVENT_EXTRAS_LOGIN_BEHAVIOR, + pendingAuthorizationRequest.loginBehavior.toString()); + extras.put(AuthorizationClient.EVENT_EXTRAS_REQUEST_CODE, pendingAuthorizationRequest.requestCode); + extras.put(AuthorizationClient.EVENT_EXTRAS_IS_LEGACY, pendingAuthorizationRequest.isLegacy); + extras.put(AuthorizationClient.EVENT_EXTRAS_PERMISSIONS, + TextUtils.join(",", pendingAuthorizationRequest.permissions)); + extras.put(AuthorizationClient.EVENT_EXTRAS_DEFAULT_AUDIENCE, + pendingAuthorizationRequest.defaultAudience.toString()); + bundle.putString(AuthorizationClient.EVENT_PARAM_EXTRAS, extras.toString()); + } catch (JSONException e) { + } + + AppEventsLogger logger = getAppEventsLogger(); + logger.logSdkEvent(AuthorizationClient.EVENT_NAME_LOGIN_START, null, bundle); + } + + private void logAuthorizationComplete(AuthorizationClient.Result.Code result, Map+ * Identifies the state of a Session. + *
+ *+ * Session objects implement a state machine that controls their lifecycle. This + * enum represents the states of the state machine. + *
+ */ +public enum SessionState { + /** + * Indicates that the Session has not yet been opened and has no cached + * token. Opening a Session in this state will involve user interaction. + */ + CREATED(Category.CREATED_CATEGORY), + + /** + *+ * Indicates that the Session has not yet been opened and has a cached + * token. Opening a Session in this state will not involve user interaction. + *
+ *+ * If you are using Session from an Android Service, you must provide a + * TokenCachingStrategy implementation that contains a valid token to the Session + * constructor. The resulting Session will be created in this state, and you + * can then safely call open, passing null for the Activity. + *
+ */ + CREATED_TOKEN_LOADED(Category.CREATED_CATEGORY), + + /** + * Indicates that the Session is in the process of opening. + */ + OPENING(Category.CREATED_CATEGORY), + + /** + * Indicates that the Session is opened. In this state, the Session may be + * used with a {@link Request}. + */ + OPENED(Category.OPENED_CATEGORY), + + /** + *+ * Indicates that the Session is opened and that the token has changed. In + * this state, the Session may be used with {@link Request}. + *
+ *+ * Every time the token is updated, {@link Session.StatusCallback + * StatusCallback} is called with this value. + *
+ */ + OPENED_TOKEN_UPDATED(Category.OPENED_CATEGORY), + + /** + * Indicates that the Session is closed, and that it was not closed + * normally. Typically this means that the open call failed, and the + * Exception parameter to {@link Session.StatusCallback StatusCallback} will + * be non-null. + */ + CLOSED_LOGIN_FAILED(Category.CLOSED_CATEGORY), + + /** + * Indicates that the Session was closed normally. + */ + CLOSED(Category.CLOSED_CATEGORY); + + private final Category category; + + SessionState(Category category) { + this.category = category; + } + + /** + * Returns a boolean indicating whether the state represents a successfully + * opened state in which the Session can be used with a {@link Request}. + * + * @return a boolean indicating whether the state represents a successfully + * opened state in which the Session can be used with a + * {@link Request}. + */ + public boolean isOpened() { + return this.category == Category.OPENED_CATEGORY; + } + + /** + * Returns a boolean indicating whether the state represents a closed + * Session that can no longer be used with a {@link Request}. + * + * @return a boolean indicating whether the state represents a closed + * Session that can no longer be used with a {@link Request}. + */ + public boolean isClosed() { + return this.category == Category.CLOSED_CATEGORY; + } + + private enum Category { + CREATED_CATEGORY, OPENED_CATEGORY, CLOSED_CATEGORY + } +} diff --git a/src/android/facebook/FacebookLib/src/com/facebook/Settings.java b/src/android/facebook/FacebookLib/src/com/facebook/Settings.java new file mode 100644 index 000000000..d964b5821 --- /dev/null +++ b/src/android/facebook/FacebookLib/src/com/facebook/Settings.java @@ -0,0 +1,501 @@ +/** + * Copyright 2010-present Facebook. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.facebook; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import com.facebook.android.BuildConfig; +import com.facebook.internal.Utility; +import com.facebook.model.GraphObject; +import com.facebook.internal.Validate; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Allows some customization of sdk behavior. + */ +public final class Settings { + private static final String TAG = Settings.class.getCanonicalName(); + private static final HashSet+ * An implementation of {@link TokenCachingStrategy TokenCachingStrategy} that uses Android SharedPreferences + * to persist information. + *
+ *
+ * The data to be cached is passed in via a Bundle. Only non-null key-value-pairs where
+ * the value is one of the following types (or an array of the same) are persisted:
+ * boolean, byte, int, long, float, double, char. In addition, String and List
+ * A base class for implementations of a {@link Session Session} token cache. + *
+ *+ * The Session constructor optionally takes a TokenCachingStrategy, from which it will + * attempt to load a cached token during construction. Also, whenever the + * Session updates its token, it will also save the token and associated state + * to the TokenCachingStrategy. + *
+ *+ * This is the only mechanism supported for an Android service to use Session. + * The service can create a custom TokenCachingStrategy that returns the Session provided + * by an Activity through which the user logged in to Facebook. + *
+ */ +public abstract class TokenCachingStrategy { + /** + * The key used by Session to store the token value in the Bundle during + * load and save. + */ + public static final String TOKEN_KEY = "com.facebook.TokenCachingStrategy.Token"; + + /** + * The key used by Session to store the expiration date value in the Bundle + * during load and save. + */ + public static final String EXPIRATION_DATE_KEY = "com.facebook.TokenCachingStrategy.ExpirationDate"; + + /** + * The key used by Session to store the last refresh date value in the + * Bundle during load and save. + */ + public static final String LAST_REFRESH_DATE_KEY = "com.facebook.TokenCachingStrategy.LastRefreshDate"; + + /** + * The key used by Session to store the user's id value in the Bundle during + * load and save. + */ + public static final String USER_FBID_KEY = "com.facebook.TokenCachingStrategy.UserFBID"; + + /** + * The key used by Session to store an enum indicating the source of the token + * in the Bundle during load and save. + */ + public static final String TOKEN_SOURCE_KEY = "com.facebook.TokenCachingStrategy.AccessTokenSource"; + + /** + * The key used by Session to store the list of permissions granted by the + * token in the Bundle during load and save. + */ + public static final String PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.Permissions"; + + private static final long INVALID_BUNDLE_MILLISECONDS = Long.MIN_VALUE; + private static final String IS_SSO_KEY = "com.facebook.TokenCachingStrategy.IsSSO"; + + /** + * Called during Session construction to get the token state. Typically this + * is loaded from a persistent store that was previously initialized via + * save. The caller may choose to keep a reference to the returned Bundle + * indefinitely. Therefore the TokenCachingStrategy should not store the returned Bundle + * and should return a new Bundle on every call to this method. + * + * @return A Bundle that represents the token state that was loaded. + */ + public abstract Bundle load(); + + /** + * Called when a Session updates its token. This is passed a Bundle of + * values that should be stored durably for the purpose of being returned + * from a later call to load. Some implementations may choose to store + * bundle beyond the scope of this call, so the caller should keep no + * references to the bundle to ensure that it is not modified later. + * + * @param bundle + * A Bundle that represents the token state to be saved. + */ + public abstract void save(Bundle bundle); + + /** + * Called when a Session learns its token is no longer valid or during a + * call to {@link Session#closeAndClearTokenInformation + * closeAndClearTokenInformation} to clear the durable state associated with + * the token. + */ + public abstract void clear(); + + /** + * Returns a boolean indicating whether a Bundle contains properties that + * could be a valid saved token. + * + * @param bundle + * A Bundle to check for token information. + * @return a boolean indicating whether a Bundle contains properties that + * could be a valid saved token. + */ + public static boolean hasTokenInformation(Bundle bundle) { + if (bundle == null) { + return false; + } + + String token = bundle.getString(TOKEN_KEY); + if ((token == null) || (token.length() == 0)) { + return false; + } + + long expiresMilliseconds = bundle.getLong(EXPIRATION_DATE_KEY, 0L); + if (expiresMilliseconds == 0L) { + return false; + } + + return true; + } + + /** + * Gets the cached token value from a Bundle. + * + * @param bundle + * A Bundle in which the token value was stored. + * @return the cached token value, or null. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static String getToken(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return bundle.getString(TOKEN_KEY); + } + + /** + * Puts the token value into a Bundle. + * + * @param bundle + * A Bundle in which the token value should be stored. + * @param value + * The String representing the token value, or null. + * + * @throws NullPointerException if the passed in Bundle or token value are null + */ + public static void putToken(Bundle bundle, String value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + bundle.putString(TOKEN_KEY, value); + } + + /** + * Gets the cached expiration date from a Bundle. + * + * @param bundle + * A Bundle in which the expiration date was stored. + * @return the cached expiration date, or null. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static Date getExpirationDate(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return getDate(bundle, EXPIRATION_DATE_KEY); + } + + /** + * Puts the expiration date into a Bundle. + * + * @param bundle + * A Bundle in which the expiration date should be stored. + * @param value + * The Date representing the expiration date. + * + * @throws NullPointerException if the passed in Bundle or date value are null + */ + public static void putExpirationDate(Bundle bundle, Date value) { + Validate.notNull(bundle, "bundle"); + Validate.notNull(value, "value"); + putDate(bundle, EXPIRATION_DATE_KEY, value); + } + + /** + * Gets the cached expiration date from a Bundle. + * + * @param bundle + * A Bundle in which the expiration date was stored. + * @return the long representing the cached expiration date in milliseconds + * since the epoch, or 0. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static long getExpirationMilliseconds(Bundle bundle) { + Validate.notNull(bundle, "bundle"); + return bundle.getLong(EXPIRATION_DATE_KEY); + } + + /** + * Puts the expiration date into a Bundle. + * + * @param bundle + * A Bundle in which the expiration date should be stored. + * @param value + * The long representing the expiration date in milliseconds + * since the epoch. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static void putExpirationMilliseconds(Bundle bundle, long value) { + Validate.notNull(bundle, "bundle"); + bundle.putLong(EXPIRATION_DATE_KEY, value); + } + + /** + * Gets the cached list of permissions from a Bundle. + * + * @param bundle + * A Bundle in which the list of permissions was stored. + * @return the cached list of permissions. + * + * @throws NullPointerException if the passed in Bundle is null + */ + public static List+ * When using this class, clients MUST call all the public methods from the + * respective methods in either an Activity or Fragment. Failure to call all the + * methods can result in improperly initialized or uninitialized Sessions. + *
+ * This class should also be used by Activities that will be displaying native dialogs + * provided by the Facebook application, in order to handle processing of the activity + * results generated by those dialogs. + */ +public class UiLifecycleHelper { + private static final String DIALOG_CALL_BUNDLE_SAVE_KEY = + "com.facebook.UiLifecycleHelper.pendingFacebookDialogCallKey"; + + private final static String ACTIVITY_NULL_MESSAGE = "activity cannot be null"; + + private final Activity activity; + private final Session.StatusCallback callback; + private final BroadcastReceiver receiver; + private final LocalBroadcastManager broadcastManager; + // Members related to handling FacebookDialog calls + private FacebookDialog.PendingCall pendingFacebookDialogCall; + private AppEventsLogger appEventsLogger; + + /** + * Creates a new UiLifecycleHelper. + * + * @param activity the Activity associated with the helper. If calling from a Fragment, + * use {@link android.support.v4.app.Fragment#getActivity()} + * @param callback the callback for Session status changes, can be null + */ + public UiLifecycleHelper(Activity activity, Session.StatusCallback callback) { + if (activity == null) { + throw new IllegalArgumentException(ACTIVITY_NULL_MESSAGE); + } + this.activity = activity; + this.callback = callback; + this.receiver = new ActiveSessionBroadcastReceiver(); + this.broadcastManager = LocalBroadcastManager.getInstance(activity); + } + + /** + * To be called from an Activity or Fragment's onCreate method. + * + * @param savedInstanceState the previously saved state + */ + public void onCreate(Bundle savedInstanceState) { + Session session = Session.getActiveSession(); + if (session == null) { + if (savedInstanceState != null) { + session = Session.restoreSession(activity, null, callback, savedInstanceState); + } + if (session == null) { + session = new Session(activity); + } + Session.setActiveSession(session); + } + if (savedInstanceState != null) { + pendingFacebookDialogCall = savedInstanceState.getParcelable(DIALOG_CALL_BUNDLE_SAVE_KEY); + } + } + + /** + * To be called from an Activity or Fragment's onResume method. + */ + public void onResume() { + Session session = Session.getActiveSession(); + if (session != null) { + if (callback != null) { + session.addCallback(callback); + } + if (SessionState.CREATED_TOKEN_LOADED.equals(session.getState())) { + session.openForRead(null); + } + } + + // add the broadcast receiver + IntentFilter filter = new IntentFilter(); + filter.addAction(Session.ACTION_ACTIVE_SESSION_SET); + filter.addAction(Session.ACTION_ACTIVE_SESSION_UNSET); + + // Add a broadcast receiver to listen to when the active Session + // is set or unset, and add/remove our callback as appropriate + broadcastManager.registerReceiver(receiver, filter); + } + + /** + * To be called from an Activity or Fragment's onActivityResult method. + * + * @param requestCode the request code + * @param resultCode the result code + * @param data the result data + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + onActivityResult(requestCode, resultCode, data, null); + } + + /** + * To be called from an Activity or Fragment's onActivityResult method, when the results of a FacebookDialog + * call are expected. + * + * @param requestCode the request code + * @param resultCode the result code + * @param data the result data + * @param dialogCallback the callback for handling FacebookDialog results, can be null + */ + public void onActivityResult(int requestCode, int resultCode, Intent data, + FacebookDialog.Callback facebookDialogCallback) { + Session session = Session.getActiveSession(); + if (session != null) { + session.onActivityResult(activity, requestCode, resultCode, data); + } + + handleFacebookDialogActivityResult(requestCode, resultCode, data, facebookDialogCallback); + } + + /** + * To be called from an Activity or Fragment's onSaveInstanceState method. + * + * @param outState the bundle to save state in + */ + public void onSaveInstanceState(Bundle outState) { + Session.saveSession(Session.getActiveSession(), outState); + outState.putParcelable(DIALOG_CALL_BUNDLE_SAVE_KEY, pendingFacebookDialogCall); + } + + /** + * To be called from an Activity or Fragment's onPause method. + */ + public void onPause() { + // remove the broadcast receiver + broadcastManager.unregisterReceiver(receiver); + + if (callback != null) { + Session session = Session.getActiveSession(); + if (session != null) { + session.removeCallback(callback); + } + } + } + + /** + * To be called from an Activity or Fragment's onStop method. + */ + public void onStop() { + AppEventsLogger.onContextStop(); + } + + /** + * To be called from an Activity or Fragment's onDestroy method. + */ + public void onDestroy() { + } + + /** + * Register that we are expecting results from a call to the Facebook application (e.g., from a native + * dialog provided by the Facebook app). Activity results forwarded to onActivityResults will be parsed + * and handled if they correspond to this call. Only a single pending FacebookDialog call can be tracked + * at a time; attempting to track another one will cancel the first one. + * @param appCall an PendingCall object containing the call ID + */ + public void trackPendingDialogCall(FacebookDialog.PendingCall pendingCall) { + if (pendingFacebookDialogCall != null) { + // If one is already pending, cancel it; we don't allow multiple pending calls. + Log.i("Facebook", "Tracking new app call while one is still pending; canceling pending call."); + cancelPendingAppCall(null); + } + pendingFacebookDialogCall = pendingCall; + } + + /** + * Retrieves an instance of AppEventsLogger that can be used for the current Session, if any. Different + * instances may be returned if the current Session changes, so this value should not be cached for long + * periods of time -- always call getAppEventsLogger to get the right logger for the current Session. If + * no Session is currently available, this method will return null. + * + * To ensure delivery of app events across Activity lifecycle events, calling Activities should be sure to + * call the onStop method. + * + * @return an AppEventsLogger to use for logging app events + */ + public AppEventsLogger getAppEventsLogger() { + Session session = Session.getActiveSession(); + if (session == null) { + return null; + } + + if (appEventsLogger == null || !appEventsLogger.isValidForSession(session)) { + if (appEventsLogger != null) { + // Pretend we got stopped so the old logger will persist its results now, in case we get stopped + // before events get flushed. + AppEventsLogger.onContextStop(); + } + appEventsLogger = AppEventsLogger.newLogger(activity, session); + } + + return appEventsLogger; + } + + /** + * The BroadcastReceiver implementation that either adds or removes the callback + * from the active Session object as it's SET or UNSET. + */ + private class ActiveSessionBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (Session.ACTION_ACTIVE_SESSION_SET.equals(intent.getAction())) { + Session session = Session.getActiveSession(); + if (session != null && callback != null) { + session.addCallback(callback); + } + } else if (Session.ACTION_ACTIVE_SESSION_UNSET.equals(intent.getAction())) { + Session session = Session.getActiveSession(); + if (session != null && callback != null) { + session.removeCallback(callback); + } + } + } + } + + private boolean handleFacebookDialogActivityResult(int requestCode, int resultCode, Intent data, + FacebookDialog.Callback facebookDialogCallback) { + if (pendingFacebookDialogCall == null || pendingFacebookDialogCall.getRequestCode() != requestCode) { + return false; + } + + if (data == null) { + // We understand the request code, but have no Intent. This can happen if the called Activity crashes + // before it can be started; we treat this as a cancellation because we have no other information. + cancelPendingAppCall(facebookDialogCallback); + return true; + } + + String callIdString = data.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID); + UUID callId = null; + if (callIdString != null) { + try { + callId = UUID.fromString(callIdString); + } catch (IllegalArgumentException exception) { + } + } + + // Was this result for the call we are waiting on? + if (callId != null && pendingFacebookDialogCall.getCallId().equals(callId)) { + // Yes, we can handle it normally. + FacebookDialog.handleActivityResult(activity, pendingFacebookDialogCall, requestCode, data, + facebookDialogCallback); + } else { + // No, send a cancellation error to the pending call and ignore the result, because we + // don't know what to do with it. + cancelPendingAppCall(facebookDialogCallback); + } + + pendingFacebookDialogCall = null; + return true; + } + + private void cancelPendingAppCall(FacebookDialog.Callback facebookDialogCallback) { + if (facebookDialogCallback != null) { + Intent pendingIntent = pendingFacebookDialogCall.getRequestIntent(); + + Intent cancelIntent = new Intent(); + cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID, + pendingIntent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_CALL_ID)); + cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION, + pendingIntent.getStringExtra(NativeProtocol.EXTRA_PROTOCOL_ACTION)); + cancelIntent.putExtra(NativeProtocol.EXTRA_PROTOCOL_VERSION, + pendingIntent.getIntExtra(NativeProtocol.EXTRA_PROTOCOL_VERSION, 0)); + cancelIntent.putExtra(NativeProtocol.STATUS_ERROR_TYPE, NativeProtocol.ERROR_UNKNOWN_ERROR); + + FacebookDialog.handleActivityResult(activity, pendingFacebookDialogCall, + pendingFacebookDialogCall.getRequestCode(), cancelIntent, facebookDialogCallback); + } + pendingFacebookDialogCall = null; + } +} diff --git a/src/android/facebook/AsyncFacebookRunner.java b/src/android/facebook/FacebookLib/src/com/facebook/android/AsyncFacebookRunner.java similarity index 91% rename from src/android/facebook/AsyncFacebookRunner.java rename to src/android/facebook/FacebookLib/src/com/facebook/android/AsyncFacebookRunner.java index be3870a1a..2420fd4b3 100644 --- a/src/android/facebook/AsyncFacebookRunner.java +++ b/src/android/facebook/FacebookLib/src/com/facebook/android/AsyncFacebookRunner.java @@ -1,5 +1,5 @@ -/* - * Copyright 2010 Facebook, Inc. +/** + * 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. @@ -16,13 +16,13 @@ package com.facebook.android; +import android.content.Context; +import android.os.Bundle; + import java.io.FileNotFoundException; import java.io.IOException; import java.net.MalformedURLException; -import android.content.Context; -import android.os.Bundle; - /** * A sample implementation of asynchronous API requests. This class provides * the ability to execute API methods and have the call return immediately, @@ -37,6 +37,8 @@ * functionality could be built, such as rate-limiting of requests, as per * a specific application's needs. * + * @deprecated + * * @see RequestListener * The callback interface. * @@ -44,6 +46,7 @@ * Yariv Sadan (yariv@fb.com), * Luke Shepard (lshepard@fb.com) */ +@Deprecated public class AsyncFacebookRunner { Facebook fb; @@ -57,10 +60,12 @@ public AsyncFacebookRunner(Facebook fb) { * memory, clearing the browser cookies, and calling auth.expireSession * through the API. The application will be notified when logout is * complete via the callback interface. - * + *
* Note that this method is asynchronous and the callback will be invoked * in a background thread; operations that affect the UI will need to be * posted to the UI thread or an appropriate handler. + * + * This method is deprecated. See {@link Facebook} and {@link com.facebook.Session} for more info. * * @param context * The Android context in which the logout should be called: it @@ -74,13 +79,14 @@ public AsyncFacebookRunner(Facebook fb) { * returns to the callback. This has no effect on the request * itself. */ + @Deprecated public void logout(final Context context, final RequestListener listener, final Object state) { new Thread() { @Override public void run() { try { - String response = fb.logout(context); + String response = fb.logoutImpl(context); if (response.length() == 0 || response.equals("false")){ listener.onFacebookError(new FacebookError( "auth.expireSession failed"), state); @@ -98,6 +104,7 @@ public void logout(final Context context, }.start(); } + @Deprecated public void logout(final Context context, final RequestListener listener) { logout(context, listener, /* state */ null); } @@ -106,19 +113,21 @@ public void logout(final Context context, final RequestListener listener) { * Make a request to Facebook's old (pre-graph) API with the given * parameters. One of the parameter keys must be "method" and its value * should be a valid REST server API method. - * + * * See http://developers.facebook.com/docs/reference/rest/ - * + * * Note that this method is asynchronous and the callback will be invoked * in a background thread; operations that affect the UI will need to be * posted to the UI thread or an appropriate handler. - * + * * Example: *
* Bundle parameters = new Bundle();
* parameters.putString("method", "auth.expireSession", new Listener());
* String response = request(parameters);
*
+ *
+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info.
*
* @param parameters
* Key-value pairs of parameters to the request. Refer to the
@@ -131,24 +140,28 @@ public void logout(final Context context, final RequestListener listener) {
* returns to the callback. This has no effect on the request
* itself.
*/
+ @Deprecated
public void request(Bundle parameters,
RequestListener listener,
final Object state) {
request(null, parameters, "GET", listener, state);
}
+ @Deprecated
public void request(Bundle parameters, RequestListener listener) {
request(null, parameters, "GET", listener, /* state */ null);
}
/**
* Make a request to the Facebook Graph API without any parameters.
- *
+ *
* See http://developers.facebook.com/docs/api
- *
+ *
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
@@ -162,12 +175,14 @@ public void request(Bundle parameters, RequestListener listener) {
* returns to the callback. This has no effect on the request
* itself.
*/
+ @Deprecated
public void request(String graphPath,
RequestListener listener,
final Object state) {
request(graphPath, new Bundle(), "GET", listener, state);
}
+ @Deprecated
public void request(String graphPath, RequestListener listener) {
request(graphPath, new Bundle(), "GET", listener, /* state */ null);
}
@@ -175,12 +190,14 @@ public void request(String graphPath, RequestListener listener) {
/**
* Make a request to the Facebook Graph API with the given string parameters
* using an HTTP GET (default method).
- *
+ *
* See http://developers.facebook.com/docs/api
- *
+ *
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
@@ -199,6 +216,7 @@ public void request(String graphPath, RequestListener listener) {
* returns to the callback. This has no effect on the request
* itself.
*/
+ @Deprecated
public void request(String graphPath,
Bundle parameters,
RequestListener listener,
@@ -206,6 +224,7 @@ public void request(String graphPath,
request(graphPath, parameters, "GET", listener, state);
}
+ @Deprecated
public void request(String graphPath,
Bundle parameters,
RequestListener listener) {
@@ -216,12 +235,14 @@ public void request(String graphPath,
* Make a request to the Facebook Graph API with the given HTTP method and
* string parameters. Note that binary data parameters (e.g. pictures) are
* not yet supported by this helper function.
- *
+ *
* See http://developers.facebook.com/docs/api
- *
+ *
* Note that this method is asynchronous and the callback will be invoked
* in a background thread; operations that affect the UI will need to be
* posted to the UI thread or an appropriate handler.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info.
*
* @param graphPath
* Path to resource in the Facebook graph, e.g., to fetch data
@@ -242,6 +263,7 @@ public void request(String graphPath,
* returns to the callback. This has no effect on the request
* itself.
*/
+ @Deprecated
public void request(final String graphPath,
final Bundle parameters,
final String httpMethod,
@@ -250,7 +272,7 @@ public void request(final String graphPath,
new Thread() {
@Override public void run() {
try {
- String resp = fb.request(graphPath, parameters, httpMethod);
+ String resp = fb.requestImpl(graphPath, parameters, httpMethod);
listener.onComplete(resp, state);
} catch (FileNotFoundException e) {
listener.onFileNotFoundException(e, state);
@@ -265,11 +287,14 @@ public void request(final String graphPath,
/**
* Callback interface for API requests.
- *
+ *
* Each method includes a 'state' parameter that identifies the calling
* request. It will be set to the value passed when originally calling the
* request method, or null if none was passed.
+ *
+ * This interface is deprecated. See {@link Facebook} and {@link com.facebook.Request} for more info.
*/
+ @Deprecated
public static interface RequestListener {
/**
diff --git a/src/android/facebook/DialogError.java b/src/android/facebook/FacebookLib/src/com/facebook/android/DialogError.java
similarity index 68%
rename from src/android/facebook/DialogError.java
rename to src/android/facebook/FacebookLib/src/com/facebook/android/DialogError.java
index 51d06c9a1..a99c4e613 100644
--- a/src/android/facebook/DialogError.java
+++ b/src/android/facebook/FacebookLib/src/com/facebook/android/DialogError.java
@@ -1,5 +1,5 @@
-/*
- * Copyright 2010 Facebook, Inc.
+/**
+ * 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.
@@ -18,8 +18,18 @@
/**
* Encapsulation of Dialog Error.
+ *
+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED.
+ *
+ * All public members of this class are intentionally deprecated.
+ * New code should instead use
+ * {@link com.facebook.FacebookException}
+ *
+ * Adding @Deprecated to this class causes warnings in other deprecated classes
+ * that reference this one. That is the only reason this entire class is not
+ * deprecated.
*
- * @author ssoneff@facebook.com
+ * @devDocDeprecated
*/
public class DialogError extends Throwable {
@@ -34,17 +44,20 @@ public class DialogError extends Throwable {
/** The URL that the dialog was trying to load */
private String mFailingUrl;
+ @Deprecated
public DialogError(String message, int errorCode, String failingUrl) {
super(message);
mErrorCode = errorCode;
mFailingUrl = failingUrl;
}
- int getErrorCode() {
+ @Deprecated
+ public int getErrorCode() {
return mErrorCode;
}
- String getFailingUrl() {
+ @Deprecated
+ public String getFailingUrl() {
return mFailingUrl;
}
diff --git a/src/android/facebook/FacebookLib/src/com/facebook/android/Facebook.java b/src/android/facebook/FacebookLib/src/com/facebook/android/Facebook.java
new file mode 100644
index 000000000..83cdabe9b
--- /dev/null
+++ b/src/android/facebook/FacebookLib/src/com/facebook/android/Facebook.java
@@ -0,0 +1,1356 @@
+/**
+ * Copyright 2010-present Facebook
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.facebook.android;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.*;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.Signature;
+import android.net.Uri;
+import android.os.*;
+import com.facebook.*;
+import com.facebook.Session.StatusCallback;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * THIS CLASS SHOULD BE CONSIDERED DEPRECATED.
+ *
+ * All public members of this class are intentionally deprecated.
+ * New code should instead use
+ * {@link Session} to manage session state,
+ * {@link Request} to make API requests, and
+ * {@link com.facebook.widget.WebDialog} to make dialog requests.
+ *
+ * Adding @Deprecated to this class causes warnings in other deprecated classes
+ * that reference this one. That is the only reason this entire class is not
+ * deprecated.
+ *
+ * @devDocDeprecated
+ */
+public class Facebook {
+
+ // Strings used in the authorization flow
+ @Deprecated
+ public static final String REDIRECT_URI = "fbconnect://success";
+ @Deprecated
+ public static final String CANCEL_URI = "fbconnect://cancel";
+ @Deprecated
+ public static final String TOKEN = "access_token";
+ @Deprecated
+ public static final String EXPIRES = "expires_in";
+ @Deprecated
+ public static final String SINGLE_SIGN_ON_DISABLED = "service_disabled";
+
+ @Deprecated
+ public static final Uri ATTRIBUTION_ID_CONTENT_URI =
+ Uri.parse("content://com.facebook.katana.provider.AttributionIdProvider");
+ @Deprecated
+ public static final String ATTRIBUTION_ID_COLUMN_NAME = "aid";
+
+ @Deprecated
+ public static final int FORCE_DIALOG_AUTH = -1;
+
+ private static final String LOGIN = "oauth";
+
+ // Used as default activityCode by authorize(). See authorize() below.
+ private static final int DEFAULT_AUTH_ACTIVITY_CODE = 32665;
+
+ // Facebook server endpoints: may be modified in a subclass for testing
+ @Deprecated
+ protected static String DIALOG_BASE_URL = "https://m.facebook.com/dialog/";
+ @Deprecated
+ protected static String GRAPH_BASE_URL = "https://graph.facebook.com/";
+ @Deprecated
+ protected static String RESTSERVER_URL = "https://api.facebook.com/restserver.php";
+
+ private final Object lock = new Object();
+
+ private String accessToken = null;
+ private long accessExpiresMillisecondsAfterEpoch = 0;
+ private long lastAccessUpdateMillisecondsAfterEpoch = 0;
+ private String mAppId;
+
+ private Activity pendingAuthorizationActivity;
+ private String[] pendingAuthorizationPermissions;
+ private Session pendingOpeningSession;
+
+ private volatile Session session; // must synchronize this.sync to write
+ private boolean sessionInvalidated; // must synchronize this.sync to access
+ private SetterTokenCachingStrategy tokenCache;
+ private volatile Session userSetSession;
+
+ // If the last time we extended the access token was more than 24 hours ago
+ // we try to refresh the access token again.
+ final private static long REFRESH_TOKEN_BARRIER = 24L * 60L * 60L * 1000L;
+
+ /**
+ * Constructor for Facebook object.
+ *
+ * @param appId
+ * Your Facebook application ID. Found at
+ * www.facebook.com/developers/apps.php.
+ */
+ @Deprecated
+ public Facebook(String appId) {
+ if (appId == null) {
+ throw new IllegalArgumentException("You must specify your application ID when instantiating "
+ + "a Facebook object. See README for details.");
+ }
+ mAppId = appId;
+ }
+
+ /**
+ * Default authorize method. Grants only basic permissions.
+ *
+ * See authorize() below for @params.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ */
+ @Deprecated
+ public void authorize(Activity activity, final DialogListener listener) {
+ authorize(activity, new String[]{}, DEFAULT_AUTH_ACTIVITY_CODE, SessionLoginBehavior.SSO_WITH_FALLBACK,
+ listener);
+ }
+
+ /**
+ * Authorize method that grants custom permissions.
+ *
+ * See authorize() below for @params.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ */
+ @Deprecated
+ public void authorize(Activity activity, String[] permissions, final DialogListener listener) {
+ authorize(activity, permissions, DEFAULT_AUTH_ACTIVITY_CODE, SessionLoginBehavior.SSO_WITH_FALLBACK, listener);
+ }
+
+ /**
+ * Full authorize method.
+ *
+ * Starts either an Activity or a dialog which prompts the user to log in to
+ * Facebook and grant the requested permissions to the given application.
+ *
+ * This method will, when possible, use Facebook's single sign-on for
+ * Android to obtain an access token. This involves proxying a call through
+ * the Facebook for Android stand-alone application, which will handle the
+ * authentication flow, and return an OAuth access token for making API
+ * calls.
+ *
+ * Because this process will not be available for all users, if single
+ * sign-on is not possible, this method will automatically fall back to the
+ * OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled
+ * by Facebook in an embedded WebView, not by the client application. As
+ * such, the dialog makes a network request and renders HTML content rather
+ * than a native UI. The access token is retrieved from a redirect to a
+ * special URL that the WebView handles.
+ *
+ * Note that User credentials could be handled natively using the OAuth 2.0
+ * Username and Password Flow, but this is not supported by this SDK.
+ *
+ * See http://developers.facebook.com/docs/authentication/ and
+ * http://wiki.oauth.net/OAuth-2 for more details.
+ *
+ * Note that this method is asynchronous and the callback will be invoked in
+ * the original calling thread (not in a background thread).
+ *
+ * Also note that requests may be made to the API without calling authorize
+ * first, in which case only public information is returned.
+ *
+ * IMPORTANT: Note that single sign-on authentication will not function
+ * correctly if you do not include a call to the authorizeCallback() method
+ * in your onActivityResult() function! Please see below for more
+ * information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH
+ * as the activityCode parameter in your call to authorize().
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ *
+ * @param activity
+ * The Android activity in which we want to display the
+ * authorization dialog.
+ * @param permissions
+ * A list of permissions required for this application: e.g.
+ * "read_stream", "publish_stream", "offline_access", etc. see
+ * http://developers.facebook.com/docs/authentication/permissions
+ * This parameter should not be null -- if you do not require any
+ * permissions, then pass in an empty String array.
+ * @param activityCode
+ * Single sign-on requires an activity result to be called back
+ * to the client application -- if you are waiting on other
+ * activities to return data, pass a custom activity code here to
+ * avoid collisions. If you would like to force the use of legacy
+ * dialog-based authorization, pass FORCE_DIALOG_AUTH for this
+ * parameter. Otherwise just omit this parameter and Facebook
+ * will use a suitable default. See
+ * http://developer.android.com/reference/android/
+ * app/Activity.html for more information.
+ * @param listener
+ * Callback interface for notifying the calling application when
+ * the authentication dialog has completed, failed, or been
+ * canceled.
+ */
+ @Deprecated
+ public void authorize(Activity activity, String[] permissions, int activityCode, final DialogListener listener) {
+ SessionLoginBehavior behavior = (activityCode >= 0) ? SessionLoginBehavior.SSO_WITH_FALLBACK
+ : SessionLoginBehavior.SUPPRESS_SSO;
+
+ authorize(activity, permissions, activityCode, behavior, listener);
+ }
+
+ /**
+ * Full authorize method.
+ *
+ * Starts either an Activity or a dialog which prompts the user to log in to
+ * Facebook and grant the requested permissions to the given application.
+ *
+ * This method will, when possible, use Facebook's single sign-on for
+ * Android to obtain an access token. This involves proxying a call through
+ * the Facebook for Android stand-alone application, which will handle the
+ * authentication flow, and return an OAuth access token for making API
+ * calls.
+ *
+ * Because this process will not be available for all users, if single
+ * sign-on is not possible, this method will automatically fall back to the
+ * OAuth 2.0 User-Agent flow. In this flow, the user credentials are handled
+ * by Facebook in an embedded WebView, not by the client application. As
+ * such, the dialog makes a network request and renders HTML content rather
+ * than a native UI. The access token is retrieved from a redirect to a
+ * special URL that the WebView handles.
+ *
+ * Note that User credentials could be handled natively using the OAuth 2.0
+ * Username and Password Flow, but this is not supported by this SDK.
+ *
+ * See http://developers.facebook.com/docs/authentication/ and
+ * http://wiki.oauth.net/OAuth-2 for more details.
+ *
+ * Note that this method is asynchronous and the callback will be invoked in
+ * the original calling thread (not in a background thread).
+ *
+ * Also note that requests may be made to the API without calling authorize
+ * first, in which case only public information is returned.
+ *
+ * IMPORTANT: Note that single sign-on authentication will not function
+ * correctly if you do not include a call to the authorizeCallback() method
+ * in your onActivityResult() function! Please see below for more
+ * information. single sign-on may be disabled by passing FORCE_DIALOG_AUTH
+ * as the activityCode parameter in your call to authorize().
+ *
+ * @param activity
+ * The Android activity in which we want to display the
+ * authorization dialog.
+ * @param permissions
+ * A list of permissions required for this application: e.g.
+ * "read_stream", "publish_stream", "offline_access", etc. see
+ * http://developers.facebook.com/docs/authentication/permissions
+ * This parameter should not be null -- if you do not require any
+ * permissions, then pass in an empty String array.
+ * @param activityCode
+ * Single sign-on requires an activity result to be called back
+ * to the client application -- if you are waiting on other
+ * activities to return data, pass a custom activity code here to
+ * avoid collisions. If you would like to force the use of legacy
+ * dialog-based authorization, pass FORCE_DIALOG_AUTH for this
+ * parameter. Otherwise just omit this parameter and Facebook
+ * will use a suitable default. See
+ * http://developer.android.com/reference/android/
+ * app/Activity.html for more information.
+ * @param behavior
+ * The {@link SessionLoginBehavior SessionLoginBehavior} that
+ * specifies what behaviors should be attempted during
+ * authorization.
+ * @param listener
+ * Callback interface for notifying the calling application when
+ * the authentication dialog has completed, failed, or been
+ * canceled.
+ */
+ private void authorize(Activity activity, String[] permissions, int activityCode,
+ SessionLoginBehavior behavior, final DialogListener listener) {
+ checkUserSession("authorize");
+ pendingOpeningSession = new Session.Builder(activity).
+ setApplicationId(mAppId).
+ setTokenCachingStrategy(getTokenCache()).
+ build();
+ pendingAuthorizationActivity = activity;
+ pendingAuthorizationPermissions = (permissions != null) ? permissions : new String[0];
+
+ StatusCallback callback = new StatusCallback() {
+ @Override
+ public void call(Session callbackSession, SessionState state, Exception exception) {
+ // Invoke user-callback.
+ onSessionCallback(callbackSession, state, exception, listener);
+ }
+ };
+
+ Session.OpenRequest openRequest = new Session.OpenRequest(activity).
+ setCallback(callback).
+ setLoginBehavior(behavior).
+ setRequestCode(activityCode).
+ setPermissions(Arrays.asList(pendingAuthorizationPermissions));
+ openSession(pendingOpeningSession, openRequest, pendingAuthorizationPermissions.length > 0);
+ }
+
+ private void openSession(Session session, Session.OpenRequest openRequest, boolean isPublish) {
+ openRequest.setIsLegacy(true);
+ if (isPublish) {
+ session.openForPublish(openRequest);
+ } else {
+ session.openForRead(openRequest);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void onSessionCallback(Session callbackSession, SessionState state, Exception exception,
+ DialogListener listener) {
+ Bundle extras = callbackSession.getAuthorizationBundle();
+
+ if (state == SessionState.OPENED) {
+ Session sessionToClose = null;
+
+ synchronized (Facebook.this.lock) {
+ if (callbackSession != Facebook.this.session) {
+ sessionToClose = Facebook.this.session;
+ Facebook.this.session = callbackSession;
+ Facebook.this.sessionInvalidated = false;
+ }
+ }
+
+ if (sessionToClose != null) {
+ sessionToClose.close();
+ }
+
+ listener.onComplete(extras);
+ } else if (exception != null) {
+ if (exception instanceof FacebookOperationCanceledException) {
+ listener.onCancel();
+ } else if ((exception instanceof FacebookAuthorizationException) && (extras != null)
+ && extras.containsKey(Session.WEB_VIEW_ERROR_CODE_KEY)
+ && extras.containsKey(Session.WEB_VIEW_FAILING_URL_KEY)) {
+ DialogError error = new DialogError(exception.getMessage(),
+ extras.getInt(Session.WEB_VIEW_ERROR_CODE_KEY),
+ extras.getString(Session.WEB_VIEW_FAILING_URL_KEY));
+ listener.onError(error);
+ } else {
+ FacebookError error = new FacebookError(exception.getMessage());
+ listener.onFacebookError(error);
+ }
+ }
+ }
+
+ /**
+ * Helper to validate a service intent by resolving and checking the
+ * provider's package signature.
+ *
+ * @param context
+ * @param intent
+ * @return true if the service intent resolution happens successfully and
+ * the signatures match.
+ */
+ private boolean validateServiceIntent(Context context, Intent intent) {
+ ResolveInfo resolveInfo = context.getPackageManager().resolveService(intent, 0);
+ if (resolveInfo == null) {
+ return false;
+ }
+
+ return validateAppSignatureForPackage(context, resolveInfo.serviceInfo.packageName);
+ }
+
+ /**
+ * Query the signature for the application that would be invoked by the
+ * given intent and verify that it matches the FB application's signature.
+ *
+ * @param context
+ * @param packageName
+ * @return true if the app's signature matches the expected signature.
+ */
+ private boolean validateAppSignatureForPackage(Context context, String packageName) {
+
+ PackageInfo packageInfo;
+ try {
+ packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+
+ for (Signature signature : packageInfo.signatures) {
+ if (signature.toCharsString().equals(FB_APP_SIGNATURE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * IMPORTANT: If you are using the deprecated authorize() method,
+ * this method must be invoked at the top of the calling
+ * activity's onActivityResult() function or Facebook authentication will
+ * not function properly!
+ *
+ * If your calling activity does not currently implement onActivityResult(),
+ * you must implement it and include a call to this method if you intend to
+ * use the authorize() method in this SDK.
+ *
+ * For more information, see
+ * http://developer.android.com/reference/android/app/
+ * Activity.html#onActivityResult(int, int, android.content.Intent)
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ */
+ @Deprecated
+ public void authorizeCallback(int requestCode, int resultCode, Intent data) {
+ checkUserSession("authorizeCallback");
+ Session pending = this.pendingOpeningSession;
+ if (pending != null) {
+ if (pending.onActivityResult(this.pendingAuthorizationActivity, requestCode, resultCode, data)) {
+ this.pendingOpeningSession = null;
+ this.pendingAuthorizationActivity = null;
+ this.pendingAuthorizationPermissions = null;
+ }
+ }
+ }
+
+ /**
+ * Refresh OAuth access token method. Binds to Facebook for Android
+ * stand-alone application application to refresh the access token. This
+ * method tries to connect to the Facebook App which will handle the
+ * authentication flow, and return a new OAuth access token. This method
+ * will automatically replace the old token with a new one. Note that this
+ * method is asynchronous and the callback will be invoked in the original
+ * calling thread (not in a background thread).
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ *
+ * @param context
+ * The Android Context that will be used to bind to the Facebook
+ * RefreshToken Service
+ * @param serviceListener
+ * Callback interface for notifying the calling application when
+ * the refresh request has completed or failed (can be null). In
+ * case of a success a new token can be found inside the result
+ * Bundle under Facebook.ACCESS_TOKEN key.
+ * @return true if the binding to the RefreshToken Service was created
+ */
+ @Deprecated
+ public boolean extendAccessToken(Context context, ServiceListener serviceListener) {
+ checkUserSession("extendAccessToken");
+ Intent intent = new Intent();
+
+ intent.setClassName("com.facebook.katana", "com.facebook.katana.platform.TokenRefreshService");
+
+ // Verify that the application whose package name is
+ // com.facebook.katana
+ // has the expected FB app signature.
+ if (!validateServiceIntent(context, intent)) {
+ return false;
+ }
+
+ return context.bindService(intent, new TokenRefreshServiceConnection(context, serviceListener),
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Calls extendAccessToken if shouldExtendAccessToken returns true.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ *
+ * @return the same value as extendAccessToken if the the token requires
+ * refreshing, true otherwise
+ */
+ @Deprecated
+ public boolean extendAccessTokenIfNeeded(Context context, ServiceListener serviceListener) {
+ checkUserSession("extendAccessTokenIfNeeded");
+ if (shouldExtendAccessToken()) {
+ return extendAccessToken(context, serviceListener);
+ }
+ return true;
+ }
+
+ /**
+ * Check if the access token requires refreshing.
+ *
+ * This method is deprecated. See {@link Facebook} and {@link Session} for more info.
+ *
+ * @return true if the last time a new token was obtained was over 24 hours
+ * ago.
+ */
+ @Deprecated
+ public boolean shouldExtendAccessToken() {
+ checkUserSession("shouldExtendAccessToken");
+ return isSessionValid()
+ && (System.currentTimeMillis() - lastAccessUpdateMillisecondsAfterEpoch >= REFRESH_TOKEN_BARRIER);
+ }
+
+ /**
+ * Handles connection to the token refresh service (this service is a part
+ * of Facebook App).
+ */
+ private class TokenRefreshServiceConnection implements ServiceConnection {
+
+ final Messenger messageReceiver = new Messenger(
+ new TokenRefreshConnectionHandler(Facebook.this, this));
+
+ final ServiceListener serviceListener;
+ final Context applicationsContext;
+
+ Messenger messageSender = null;
+
+ public TokenRefreshServiceConnection(Context applicationsContext, ServiceListener serviceListener) {
+ this.applicationsContext = applicationsContext;
+ this.serviceListener = serviceListener;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ messageSender = new Messenger(service);
+ refreshToken();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg) {
+ serviceListener.onError(new Error("Service disconnected"));
+ // We returned an error so there's no point in
+ // keeping the binding open.
+ applicationsContext.unbindService(TokenRefreshServiceConnection.this);
+ }
+
+ private void refreshToken() {
+ Bundle requestData = new Bundle();
+ requestData.putString(TOKEN, accessToken);
+
+ Message request = Message.obtain();
+ request.setData(requestData);
+ request.replyTo = messageReceiver;
+
+ try {
+ messageSender.send(request);
+ } catch (RemoteException e) {
+ serviceListener.onError(new Error("Service connection error"));
+ }
+ }
+ }
+
+ // Creating a static Handler class to reduce the possibility of a memory leak.
+ // Handler objects for the same thread all share a common Looper object, which they post messages
+ // to and read from. As messages contain target Handler, as long as there are messages with target
+ // handler in the message queue, the handler cannot be garbage collected. If handler is not static,
+ // the instance of the containing class also cannot be garbage collected even if it is destroyed.
+ private static class TokenRefreshConnectionHandler extends Handler {
+ WeakReference