/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook.share.internal; import android.app.Activity; import android.content.*; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.v4.app.Fragment; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.*; import com.facebook.appevents.AppEventsLogger; import com.facebook.internal.AnalyticsEvents; import com.facebook.internal.AppCall; import com.facebook.internal.BundleJSONConverter; import com.facebook.internal.CallbackManagerImpl; import com.facebook.internal.FileLruCache; import com.facebook.internal.Logger; import com.facebook.internal.NativeProtocol; import com.facebook.internal.ServerProtocol; import com.facebook.internal.Utility; import com.facebook.internal.WorkQueue; import com.facebook.share.widget.LikeView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; /** * com.facebook.share.internal is solely for the use of other packages within the Facebook SDK for * Android. Use of any of the classes in this package is unsupported, and they may be modified or * removed without warning at any time. */ public class LikeActionController { public static final String ACTION_LIKE_ACTION_CONTROLLER_UPDATED = "com.facebook.sdk.LikeActionController.UPDATED"; public static final String ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR = "com.facebook.sdk.LikeActionController.DID_ERROR"; public static final String ACTION_LIKE_ACTION_CONTROLLER_DID_RESET = "com.facebook.sdk.LikeActionController.DID_RESET"; public static final String ACTION_OBJECT_ID_KEY = "com.facebook.sdk.LikeActionController.OBJECT_ID"; public static final String ERROR_INVALID_OBJECT_ID = "Invalid Object Id"; public static final String ERROR_PUBLISH_ERROR = "Unable to publish the like/unlike action"; private static final String TAG = LikeActionController.class.getSimpleName(); private static final int LIKE_ACTION_CONTROLLER_VERSION = 3; private static final int MAX_CACHE_SIZE = 128; // MAX_OBJECT_SUFFIX basically accommodates for 1000 access token changes before the async // disk-cache-clear finishes. The value is reasonably arbitrary. private static final int MAX_OBJECT_SUFFIX = 1000; private static final String LIKE_ACTION_CONTROLLER_STORE = "com.facebook.LikeActionController.CONTROLLER_STORE_KEY"; private static final String LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY = "PENDING_CONTROLLER_KEY"; private static final String LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY = "OBJECT_SUFFIX"; private static final String JSON_INT_VERSION_KEY = "com.facebook.share.internal.LikeActionController.version"; private static final String JSON_STRING_OBJECT_ID_KEY = "object_id"; private static final String JSON_INT_OBJECT_TYPE_KEY = "object_type"; private static final String JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY = "like_count_string_with_like"; private static final String JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY = "like_count_string_without_like"; private static final String JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY = "social_sentence_with_like"; private static final String JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY = "social_sentence_without_like"; private static final String JSON_BOOL_IS_OBJECT_LIKED_KEY = "is_object_liked"; private static final String JSON_STRING_UNLIKE_TOKEN_KEY = "unlike_token"; private static final String JSON_BUNDLE_FACEBOOK_DIALOG_ANALYTICS_BUNDLE = "facebook_dialog_analytics_bundle"; private static final String LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY = "object_is_liked"; private static final String LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY = "like_count_string"; private static final String LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY = "social_sentence"; private static final String LIKE_DIALOG_RESPONSE_UNLIKE_TOKEN_KEY = "unlike_token"; private static final int ERROR_CODE_OBJECT_ALREADY_LIKED = 3501; private static FileLruCache controllerDiskCache; private static final ConcurrentHashMap<String, LikeActionController> cache = new ConcurrentHashMap<>(); // This MUST be 1 for proper synchronization private static WorkQueue mruCacheWorkQueue = new WorkQueue(1); // This MUST be 1 for proper synchronization private static WorkQueue diskIOWorkQueue = new WorkQueue(1); private static Handler handler; private static String objectIdForPendingController; private static boolean isInitialized; private static volatile int objectSuffix; private static AccessTokenTracker accessTokenTracker; private String objectId; private LikeView.ObjectType objectType; private boolean isObjectLiked; private String likeCountStringWithLike; private String likeCountStringWithoutLike; private String socialSentenceWithLike; private String socialSentenceWithoutLike; private String unlikeToken; private String verifiedObjectId; private boolean objectIsPage; private boolean isObjectLikedOnServer; private boolean isPendingLikeOrUnlike; private Bundle facebookDialogAnalyticsBundle; private AppEventsLogger appEventsLogger; /** * Called from CallbackManager to process any pending likes that had resulted in the Like * dialog being displayed * * @param requestCode From the originating call to onActivityResult * @param resultCode From the originating call to onActivityResult * @param data From the originating call to onActivityResult * @return Indication of whether the Intent was handled */ public static boolean handleOnActivityResult(final int requestCode, final int resultCode, final Intent data) { // See if we were waiting on a Like dialog completion. if (Utility.isNullOrEmpty(objectIdForPendingController)) { Context appContext = FacebookSdk.getApplicationContext(); SharedPreferences sharedPreferences = appContext.getSharedPreferences( LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE); objectIdForPendingController = sharedPreferences.getString( LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY, null); } if (Utility.isNullOrEmpty(objectIdForPendingController)) { // Doesn't look like we were waiting on a Like dialog completion return false; } getControllerForObjectId( objectIdForPendingController, LikeView.ObjectType.UNKNOWN, new CreationCallback() { @Override public void onComplete( LikeActionController likeActionController, FacebookException error) { if (error == null) { likeActionController.onActivityResult( requestCode, resultCode, data); } else { Utility.logd(TAG, error); } } }); return true; } /** * Called by the LikeView when an object-id is set on it. * * @param objectId Object Id * @param callback Callback to be invoked when the LikeActionController has been created. */ public static void getControllerForObjectId( String objectId, LikeView.ObjectType objectType, CreationCallback callback) { if (!isInitialized) { performFirstInitialize(); } LikeActionController controllerForObject = getControllerFromInMemoryCache(objectId); if (controllerForObject != null) { // Direct object-cache hit verifyControllerAndInvokeCallback(controllerForObject, objectType, callback); } else { diskIOWorkQueue.addActiveWorkItem( new CreateLikeActionControllerWorkItem(objectId, objectType, callback)); } } private static void verifyControllerAndInvokeCallback( LikeActionController likeActionController, LikeView.ObjectType objectType, CreationCallback callback) { LikeView.ObjectType bestObjectType = ShareInternalUtility.getMostSpecificObjectType( objectType, likeActionController.objectType); FacebookException error = null; if (bestObjectType == null) { // Looks like the existing controller has an object_type for this object_id that is // not compatible with the requested object type. error = new FacebookException( "Object with id:\"%s\" is already marked as type:\"%s\". " + "Cannot change the type to:\"%s\"", likeActionController.objectId, likeActionController.objectType.toString(), objectType.toString()); likeActionController = null; } else { likeActionController.objectType = bestObjectType; } invokeCallbackWithController(callback, likeActionController, error); } /** * NOTE: This MUST be called ONLY via the CreateLikeActionControllerWorkItem class to ensure * that it happens on the right thread, at the right time. */ private static void createControllerForObjectIdAndType( String objectId, LikeView.ObjectType objectType, CreationCallback callback) { // Check again to see if the controller was created before attempting to deserialize/create // one. Need to check this in the case where multiple LikeViews are looking for a controller // for the same object and all got queued up to create one. We only want the first one to go // through with the creation, and the rest should get the same instance from the // object-cache. LikeActionController controllerForObject = getControllerFromInMemoryCache(objectId); if (controllerForObject != null) { // Direct object-cache hit verifyControllerAndInvokeCallback(controllerForObject, objectType, callback); return; } // Try deserialize from disk controllerForObject = deserializeFromDiskSynchronously(objectId); if (controllerForObject == null) { controllerForObject = new LikeActionController(objectId, objectType); serializeToDiskAsync(controllerForObject); } // Update object-cache. putControllerInMemoryCache(objectId, controllerForObject); // Refresh the controller on the Main thread. final LikeActionController controllerToRefresh = controllerForObject; handler.post(new Runnable() { @Override public void run() { controllerToRefresh.refreshStatusAsync(); } }); invokeCallbackWithController(callback, controllerToRefresh, null); } private synchronized static void performFirstInitialize() { if (isInitialized) { return; } handler = new Handler(Looper.getMainLooper()); Context appContext = FacebookSdk.getApplicationContext(); SharedPreferences sharedPreferences = appContext.getSharedPreferences( LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE); objectSuffix = sharedPreferences.getInt(LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY, 1); controllerDiskCache = new FileLruCache(TAG, new FileLruCache.Limits()); registerAccessTokenTracker(); CallbackManagerImpl.registerStaticCallback( CallbackManagerImpl.RequestCodeOffset.Like.toRequestCode(), new CallbackManagerImpl.Callback() { @Override public boolean onActivityResult(int resultCode, Intent data) { return handleOnActivityResult( CallbackManagerImpl.RequestCodeOffset.Like.toRequestCode(), resultCode, data); } }); isInitialized = true; } private static void invokeCallbackWithController( final CreationCallback callback, final LikeActionController controller, final FacebookException error) { if (callback == null) { return; } handler.post(new Runnable() { @Override public void run() { callback.onComplete(controller, error); } }); } // // In-memory mru-caching code // private static void registerAccessTokenTracker() { accessTokenTracker = new AccessTokenTracker() { @Override protected void onCurrentAccessTokenChanged( AccessToken oldAccessToken, AccessToken currentAccessToken) { Context appContext = FacebookSdk.getApplicationContext(); if (currentAccessToken == null) { // Bump up the objectSuffix so that we don't have a filename collision between a // cache-clear and and a cache-read/write. // // NOTE: We know that onReceive() was called on the main thread. This means that // even this code is running on the main thread, and therefore, there aren't // synchronization issues with incrementing the objectSuffix and clearing the // caches here. objectSuffix = (objectSuffix + 1) % MAX_OBJECT_SUFFIX; appContext.getSharedPreferences( LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE) .edit() .putInt(LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY, objectSuffix) .apply(); // Only clearing the actual caches. The MRU index will self-clean with usage. // Clearing the caches is necessary to prevent leaking like-state across // users. cache.clear(); controllerDiskCache.clearCache(); } broadcastAction(null, ACTION_LIKE_ACTION_CONTROLLER_DID_RESET); } }; } private static void putControllerInMemoryCache( String objectId, LikeActionController controllerForObject) { String cacheKey = getCacheKeyForObjectId(objectId); // Move this object to the front. Also trim cache if necessary mruCacheWorkQueue.addActiveWorkItem(new MRUCacheWorkItem(cacheKey, true)); cache.put(cacheKey, controllerForObject); } private static LikeActionController getControllerFromInMemoryCache(String objectId) { String cacheKey = getCacheKeyForObjectId(objectId); LikeActionController controller = cache.get(cacheKey); if (controller != null) { // Move this object to the front mruCacheWorkQueue.addActiveWorkItem(new MRUCacheWorkItem(cacheKey, false)); } return controller; } // // Disk caching code // private static void serializeToDiskAsync(LikeActionController controller) { String controllerJson = serializeToJson(controller); String cacheKey = getCacheKeyForObjectId(controller.objectId); if (!Utility.isNullOrEmpty(controllerJson) && !Utility.isNullOrEmpty(cacheKey)) { diskIOWorkQueue.addActiveWorkItem( new SerializeToDiskWorkItem(cacheKey, controllerJson)); } } /** * NOTE: This MUST be called ONLY via the SerializeToDiskWorkItem class to ensure that it * happens on the right thread, at the right time. */ private static void serializeToDiskSynchronously(String cacheKey, String controllerJson) { OutputStream outputStream = null; try { outputStream = controllerDiskCache.openPutStream(cacheKey); outputStream.write(controllerJson.getBytes()); } catch (IOException e) { Log.e(TAG, "Unable to serialize controller to disk", e); } finally { if (outputStream != null) { Utility.closeQuietly(outputStream); } } } /** * NOTE: This MUST be called ONLY via the CreateLikeActionControllerWorkItem class to ensure * that it happens on the right thread, at the right time. */ private static LikeActionController deserializeFromDiskSynchronously(String objectId) { LikeActionController controller = null; InputStream inputStream = null; try { String cacheKey = getCacheKeyForObjectId(objectId); inputStream = controllerDiskCache.get(cacheKey); if (inputStream != null) { String controllerJsonString = Utility.readStreamToString(inputStream); if (!Utility.isNullOrEmpty(controllerJsonString)) { controller = deserializeFromJson(controllerJsonString); } } } catch (IOException e) { Log.e(TAG, "Unable to deserialize controller from disk", e); controller = null; } finally { if (inputStream != null) { Utility.closeQuietly(inputStream); } } return controller; } private static LikeActionController deserializeFromJson(String controllerJsonString) { LikeActionController controller; try { JSONObject controllerJson = new JSONObject(controllerJsonString); int version = controllerJson.optInt(JSON_INT_VERSION_KEY, -1); if (version != LIKE_ACTION_CONTROLLER_VERSION) { // Don't attempt to deserialize a controller that might be serialized differently // than expected. return null; } String objectId = controllerJson.getString(JSON_STRING_OBJECT_ID_KEY); int objectTypeInt = controllerJson.optInt( JSON_INT_OBJECT_TYPE_KEY, LikeView.ObjectType.UNKNOWN.getValue()); controller = new LikeActionController( objectId, LikeView.ObjectType.fromInt(objectTypeInt)); // Make sure to default to null and not empty string, to keep the logic elsewhere // functioning properly. controller.likeCountStringWithLike = controllerJson.optString(JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY, null); controller.likeCountStringWithoutLike = controllerJson.optString(JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY, null); controller.socialSentenceWithLike = controllerJson.optString(JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY, null); controller.socialSentenceWithoutLike = controllerJson.optString(JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY, null); controller.isObjectLiked = controllerJson.optBoolean(JSON_BOOL_IS_OBJECT_LIKED_KEY); controller.unlikeToken = controllerJson.optString(JSON_STRING_UNLIKE_TOKEN_KEY, null); JSONObject analyticsJSON = controllerJson.optJSONObject( JSON_BUNDLE_FACEBOOK_DIALOG_ANALYTICS_BUNDLE); if (analyticsJSON != null) { controller.facebookDialogAnalyticsBundle = BundleJSONConverter.convertToBundle(analyticsJSON); } } catch (JSONException e) { Log.e(TAG, "Unable to deserialize controller from JSON", e); controller = null; } return controller; } private static String serializeToJson(LikeActionController controller) { JSONObject controllerJson = new JSONObject(); try { controllerJson.put(JSON_INT_VERSION_KEY, LIKE_ACTION_CONTROLLER_VERSION); controllerJson.put(JSON_STRING_OBJECT_ID_KEY, controller.objectId); controllerJson.put(JSON_INT_OBJECT_TYPE_KEY, controller.objectType.getValue()); controllerJson.put( JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY, controller.likeCountStringWithLike); controllerJson.put( JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY, controller.likeCountStringWithoutLike); controllerJson.put( JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY, controller.socialSentenceWithLike); controllerJson.put( JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY, controller.socialSentenceWithoutLike); controllerJson.put(JSON_BOOL_IS_OBJECT_LIKED_KEY, controller.isObjectLiked); controllerJson.put(JSON_STRING_UNLIKE_TOKEN_KEY, controller.unlikeToken); if (controller.facebookDialogAnalyticsBundle != null) { JSONObject analyticsJSON = BundleJSONConverter.convertToJSON( controller.facebookDialogAnalyticsBundle); if (analyticsJSON != null) { controllerJson.put( JSON_BUNDLE_FACEBOOK_DIALOG_ANALYTICS_BUNDLE, analyticsJSON); } } } catch (JSONException e) { Log.e(TAG, "Unable to serialize controller to JSON", e); return null; } return controllerJson.toString(); } private static String getCacheKeyForObjectId(String objectId) { String accessTokenPortion = null; AccessToken accessToken = AccessToken.getCurrentAccessToken(); if (accessToken != null) { accessTokenPortion = accessToken.getToken(); } if (accessTokenPortion != null) { // Cache-key collisions are not something to worry about here, since we only store state // for one access token. Even in the case where the previous access tokens serialized // files have not been deleted yet, the objectSuffix will be different due to the access // token change, thus making the key different. accessTokenPortion = Utility.md5hash(accessTokenPortion); } return String.format( Locale.ROOT, "%s|%s|com.fb.sdk.like|%d", objectId, Utility.coerceValueIfNullOrEmpty(accessTokenPortion, ""), objectSuffix); } // // Broadcast handling code // private static void broadcastAction( LikeActionController controller, String action) { broadcastAction(controller, action, null); } private static void broadcastAction( LikeActionController controller, String action, Bundle data) { Intent broadcastIntent = new Intent(action); if (controller != null) { if (data == null) { data = new Bundle(); } data.putString(ACTION_OBJECT_ID_KEY, controller.getObjectId()); } if (data != null) { broadcastIntent.putExtras(data); } LocalBroadcastManager.getInstance(FacebookSdk.getApplicationContext()) .sendBroadcast(broadcastIntent); } /** * Constructor */ private LikeActionController(String objectId, LikeView.ObjectType objectType) { this.objectId = objectId; this.objectType = objectType; } /** * Gets the the associated object id * * @return object id */ public String getObjectId() { return objectId; } /** * Gets the String representation of the like-count for the associated object * * @return String representation of the like-count for the associated object */ public String getLikeCountString() { return isObjectLiked ? likeCountStringWithLike : likeCountStringWithoutLike; } /** * Gets the String representation of the like-count for the associated object * * @return String representation of the like-count for the associated object */ public String getSocialSentence() { return isObjectLiked ? socialSentenceWithLike : socialSentenceWithoutLike; } /** * Indicates whether the associated object is liked * * @return Indication of whether the associated object is liked */ public boolean isObjectLiked() { return isObjectLiked; } /** * Indicates whether the LikeView should enable itself. * * @return Indication of whether the LikeView should enable itself. */ public boolean shouldEnableView() { if (LikeDialog.canShowNativeDialog() || LikeDialog.canShowWebFallback()) { return true; } if (objectIsPage || (objectType == LikeView.ObjectType.PAGE)) { // If we can't use the dialogs, then we can't like Pages. // Before any requests are made to the server, we have to rely on the object type set // by the app. If we have permissions to make requests, we will know the real type after // the first request. return false; } // See if we have publish permissions. // NOTE: This will NOT be accurate if the app has the type set as UNKNOWN, and the // underlying object is a page. AccessToken token = AccessToken.getCurrentAccessToken(); return token != null && token.getPermissions() != null && token.getPermissions().contains("publish_actions"); } /** * Entry-point to the code that performs the like/unlike action. */ public void toggleLike(Activity activity, Fragment fragment, Bundle analyticsParameters) { getAppEventsLogger().logSdkEvent( AnalyticsEvents.EVENT_LIKE_VIEW_DID_TAP, null, analyticsParameters); boolean shouldLikeObject = !this.isObjectLiked; if (canUseOGPublish()) { // Update UI Like state optimistically updateLikeState(shouldLikeObject); if (isPendingLikeOrUnlike) { // If the user toggled the button quickly, and there is still a publish underway, // don't fire off another request. Also log this behavior. getAppEventsLogger().logSdkEvent( AnalyticsEvents.EVENT_LIKE_VIEW_DID_UNDO_QUICKLY, null, analyticsParameters); } else if (!publishLikeOrUnlikeAsync(shouldLikeObject, analyticsParameters)) { // We were not able to send a graph request to unlike or like the object // Undo the optimistic state-update and show the dialog instead updateLikeState(!shouldLikeObject); presentLikeDialog(activity, fragment, analyticsParameters); } } else { presentLikeDialog(activity, fragment, analyticsParameters); } } private AppEventsLogger getAppEventsLogger() { if (appEventsLogger == null) { appEventsLogger = AppEventsLogger.newLogger(FacebookSdk.getApplicationContext()); } return appEventsLogger; } private boolean publishLikeOrUnlikeAsync( boolean shouldLikeObject, Bundle analyticsParameters) { boolean requested = false; if (canUseOGPublish()) { if (shouldLikeObject) { requested = true; publishLikeAsync(analyticsParameters); } else if (!Utility.isNullOrEmpty(this.unlikeToken)) { requested = true; publishUnlikeAsync(analyticsParameters); } } return requested; } /** * Only to be called after an OG-publish was attempted and something went wrong. The Button * state is reverted and an error is returned to the LikeViews */ private void publishDidError(boolean oldLikeState) { updateLikeState(oldLikeState); Bundle errorBundle = new Bundle(); errorBundle.putString( NativeProtocol.STATUS_ERROR_DESCRIPTION, ERROR_PUBLISH_ERROR); broadcastAction( LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, errorBundle); } private void updateLikeState(boolean isObjectLiked) { updateState(isObjectLiked, this.likeCountStringWithLike, this.likeCountStringWithoutLike, this.socialSentenceWithLike, this.socialSentenceWithoutLike, this.unlikeToken); } private void updateState(boolean isObjectLiked, String likeCountStringWithLike, String likeCountStringWithoutLike, String socialSentenceWithLike, String socialSentenceWithoutLike, String unlikeToken) { // Normalize all empty strings to null, so that we don't have any problems with comparison. likeCountStringWithLike = Utility.coerceValueIfNullOrEmpty(likeCountStringWithLike, null); likeCountStringWithoutLike = Utility.coerceValueIfNullOrEmpty(likeCountStringWithoutLike, null); socialSentenceWithLike = Utility.coerceValueIfNullOrEmpty(socialSentenceWithLike, null); socialSentenceWithoutLike = Utility.coerceValueIfNullOrEmpty(socialSentenceWithoutLike, null); unlikeToken = Utility.coerceValueIfNullOrEmpty(unlikeToken, null); boolean stateChanged = isObjectLiked != this.isObjectLiked || !Utility.areObjectsEqual( likeCountStringWithLike, this.likeCountStringWithLike) || !Utility.areObjectsEqual( likeCountStringWithoutLike, this.likeCountStringWithoutLike) || !Utility.areObjectsEqual(socialSentenceWithLike, this.socialSentenceWithLike) || !Utility.areObjectsEqual( socialSentenceWithoutLike, this.socialSentenceWithoutLike) || !Utility.areObjectsEqual(unlikeToken, this.unlikeToken); if (!stateChanged) { return; } this.isObjectLiked = isObjectLiked; this.likeCountStringWithLike = likeCountStringWithLike; this.likeCountStringWithoutLike = likeCountStringWithoutLike; this.socialSentenceWithLike = socialSentenceWithLike; this.socialSentenceWithoutLike = socialSentenceWithoutLike; this.unlikeToken = unlikeToken; serializeToDiskAsync(this); broadcastAction(this, ACTION_LIKE_ACTION_CONTROLLER_UPDATED); } private void presentLikeDialog( final Activity activity, final Fragment fragment, final Bundle analyticsParameters) { String analyticsEvent = null; if (LikeDialog.canShowNativeDialog()) { analyticsEvent = AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_DIALOG; } else if (LikeDialog.canShowWebFallback()) { analyticsEvent = AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_FALLBACK; } else { // We will get here if the user tapped the button when dialogs cannot be shown. logAppEventForError("present_dialog", analyticsParameters); Utility.logd(TAG, "Cannot show the Like Dialog on this device."); // If we got to this point, we should ask the views to check if they should now // be disabled. broadcastAction(null, ACTION_LIKE_ACTION_CONTROLLER_UPDATED); } // Using the value of analyticsEvent to see if we can show any version of the dialog. // Written this way just to prevent extra lines of code. if (analyticsEvent != null) { LikeContent likeContent = new LikeContent.Builder() .setObjectId(this.objectId) .setObjectType(this.objectType) .build(); if (fragment != null) { new LikeDialog(fragment).show(likeContent); } else { new LikeDialog(activity).show(likeContent); } saveState(analyticsParameters); getAppEventsLogger().logSdkEvent( AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_DIALOG, null, analyticsParameters); } } private void onActivityResult( int requestCode, int resultCode, Intent data) { // Look for results ShareInternalUtility.handleActivityResult( requestCode, resultCode, data, getResultProcessor(facebookDialogAnalyticsBundle)); // The handlers from above will run synchronously. So by the time we get here, it should be // safe to stop tracking this call and also serialize the controller to disk clearState(); } private ResultProcessor getResultProcessor(final Bundle analyticsParameters) { return new ResultProcessor(null) { @Override public void onSuccess(AppCall appCall, Bundle data) { if (data == null || !data.containsKey(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY)) { // This is an empty result that we can't handle. return; } boolean isObjectLiked = data.getBoolean(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY); // Default to known/cached state, if properties are missing. String likeCountStringWithLike = LikeActionController.this.likeCountStringWithLike; String likeCountStringWithoutLike = LikeActionController.this.likeCountStringWithoutLike; if (data.containsKey(LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY)) { likeCountStringWithLike = data.getString(LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY); likeCountStringWithoutLike = likeCountStringWithLike; } String socialSentenceWithLike = LikeActionController.this.socialSentenceWithLike; String socialSentenceWithoutWithoutLike = LikeActionController.this.socialSentenceWithoutLike; if (data.containsKey(LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY)) { socialSentenceWithLike = data.getString( LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY); socialSentenceWithoutWithoutLike = socialSentenceWithLike; } String unlikeToken = data.containsKey(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY) ? data.getString(LIKE_DIALOG_RESPONSE_UNLIKE_TOKEN_KEY) : LikeActionController.this.unlikeToken; Bundle logParams = (analyticsParameters == null) ? new Bundle() : analyticsParameters; logParams.putString( AnalyticsEvents.PARAMETER_CALL_ID, appCall.getCallId().toString()); getAppEventsLogger().logSdkEvent( AnalyticsEvents.EVENT_LIKE_VIEW_DIALOG_DID_SUCCEED, null, logParams); updateState( isObjectLiked, likeCountStringWithLike, likeCountStringWithoutLike, socialSentenceWithLike, socialSentenceWithoutWithoutLike, unlikeToken); } @Override public void onError(AppCall appCall, FacebookException error) { Logger.log( LoggingBehavior.REQUESTS, TAG, "Like Dialog failed with error : %s", error); Bundle logParams = analyticsParameters == null ? new Bundle() : analyticsParameters; logParams.putString( AnalyticsEvents.PARAMETER_CALL_ID, appCall.getCallId().toString()); // Log the error and AppEvent logAppEventForError("present_dialog", logParams); broadcastAction( LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, NativeProtocol.createBundleForException(error)); } @Override public void onCancel(AppCall appCall) { onError(appCall, new FacebookOperationCanceledException()); } }; } private void saveState(Bundle analyticsParameters) { // Save off the call id for processing the response storeObjectIdForPendingController(objectId); // Store off the analytics parameters as well, for completion-logging facebookDialogAnalyticsBundle = analyticsParameters; // Serialize to disk, in case we get terminated while waiting for the dialog to complete serializeToDiskAsync(this); } private void clearState() { facebookDialogAnalyticsBundle = null; storeObjectIdForPendingController(null); } private static void storeObjectIdForPendingController(String objectId) { objectIdForPendingController = objectId; Context appContext = FacebookSdk.getApplicationContext(); appContext.getSharedPreferences(LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE) .edit() .putString( LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY, objectIdForPendingController) .apply(); } private boolean canUseOGPublish() { AccessToken accessToken = AccessToken.getCurrentAccessToken(); // Verify that the object isn't a Page, that we have permissions and that, if we're // unliking, then we have an unlike token. return !objectIsPage && verifiedObjectId != null && accessToken != null && accessToken.getPermissions() != null && accessToken.getPermissions().contains("publish_actions"); } private void publishLikeAsync(final Bundle analyticsParameters) { isPendingLikeOrUnlike = true; fetchVerifiedObjectId(new RequestCompletionCallback() { @Override public void onComplete() { if (Utility.isNullOrEmpty(verifiedObjectId)) { // Could not get a verified id Bundle errorBundle = new Bundle(); errorBundle.putString( NativeProtocol.STATUS_ERROR_DESCRIPTION, ERROR_INVALID_OBJECT_ID); broadcastAction( LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, errorBundle); return; } // Perform the Like. GraphRequestBatch requestBatch = new GraphRequestBatch(); final PublishLikeRequestWrapper likeRequest = new PublishLikeRequestWrapper(verifiedObjectId, objectType); likeRequest.addToBatch(requestBatch); requestBatch.addCallback(new GraphRequestBatch.Callback() { @Override public void onBatchCompleted(GraphRequestBatch batch) { isPendingLikeOrUnlike = false; if (likeRequest.error != null) { // We already updated the UI to show button in the Liked state. Since // this failed, let's revert back to the Unliked state and broadcast // an error publishDidError(false); } else { unlikeToken = Utility.coerceValueIfNullOrEmpty(likeRequest.unlikeToken, null); isObjectLikedOnServer = true; getAppEventsLogger().logSdkEvent( AnalyticsEvents.EVENT_LIKE_VIEW_DID_LIKE, null, analyticsParameters); // See if the user toggled the button back while this request was // completing publishAgainIfNeeded(analyticsParameters); } } }); requestBatch.executeAsync(); } }); } private void publishUnlikeAsync(final Bundle analyticsParameters) { isPendingLikeOrUnlike = true; // Perform the Unlike. GraphRequestBatch requestBatch = new GraphRequestBatch(); final PublishUnlikeRequestWrapper unlikeRequest = new PublishUnlikeRequestWrapper(unlikeToken); unlikeRequest.addToBatch(requestBatch); requestBatch.addCallback(new GraphRequestBatch.Callback() { @Override public void onBatchCompleted(GraphRequestBatch batch) { isPendingLikeOrUnlike = false; if (unlikeRequest.error != null) { // We already updated the UI to show button in the Unliked state. Since this // failed, let's revert back to the Liked state and broadcast an error. publishDidError(true); } else { unlikeToken = null; isObjectLikedOnServer = false; getAppEventsLogger().logSdkEvent( AnalyticsEvents.EVENT_LIKE_VIEW_DID_UNLIKE, null, analyticsParameters); // See if the user toggled the button back while this request was // completing publishAgainIfNeeded(analyticsParameters); } } }); requestBatch.executeAsync(); } private void refreshStatusAsync() { AccessToken accessToken = AccessToken.getCurrentAccessToken(); if (accessToken == null) { // Only when we know that there is no active access token should we attempt getting like // state from the service. Otherwise, use the access token to make sure we get the // correct like state. refreshStatusViaService(); return; } fetchVerifiedObjectId(new RequestCompletionCallback() { @Override public void onComplete() { final GetOGObjectLikesRequestWrapper objectLikesRequest = new GetOGObjectLikesRequestWrapper(verifiedObjectId, objectType); final GetEngagementRequestWrapper engagementRequest = new GetEngagementRequestWrapper(verifiedObjectId, objectType); GraphRequestBatch requestBatch = new GraphRequestBatch(); objectLikesRequest.addToBatch(requestBatch); engagementRequest.addToBatch(requestBatch); requestBatch.addCallback(new GraphRequestBatch.Callback() { @Override public void onBatchCompleted(GraphRequestBatch batch) { if (objectLikesRequest.error != null || engagementRequest.error != null) { // Refreshing is best-effort. If the refresh fails, don't lose old // state. Logger.log( LoggingBehavior.REQUESTS, TAG, "Unable to refresh like state for id: '%s'", objectId); return; } updateState( objectLikesRequest.objectIsLiked, engagementRequest.likeCountStringWithLike, engagementRequest.likeCountStringWithoutLike, engagementRequest.socialSentenceStringWithLike, engagementRequest.socialSentenceStringWithoutLike, objectLikesRequest.unlikeToken); } }); requestBatch.executeAsync(); } }); } private void refreshStatusViaService() { LikeStatusClient likeStatusClient = new LikeStatusClient( FacebookSdk.getApplicationContext(), FacebookSdk.getApplicationId(), objectId); if (!likeStatusClient.start()) { return; } LikeStatusClient.CompletedListener callback = new LikeStatusClient.CompletedListener() { @Override public void completed(Bundle result) { // Don't lose old state if the service response is incomplete. if (result == null || !result.containsKey(ShareConstants.EXTRA_OBJECT_IS_LIKED)) { return; } boolean objectIsLiked = result.getBoolean(ShareConstants.EXTRA_OBJECT_IS_LIKED); String likeCountWithLike = result.containsKey(ShareConstants.EXTRA_LIKE_COUNT_STRING_WITH_LIKE) ? result.getString(ShareConstants.EXTRA_LIKE_COUNT_STRING_WITH_LIKE) : LikeActionController.this.likeCountStringWithLike; String likeCountWithoutLike = result.containsKey(ShareConstants.EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE) ? result.getString( ShareConstants.EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE) : LikeActionController.this.likeCountStringWithoutLike; String socialSentenceWithLike = result.containsKey(ShareConstants.EXTRA_SOCIAL_SENTENCE_WITH_LIKE) ? result.getString(ShareConstants.EXTRA_SOCIAL_SENTENCE_WITH_LIKE) : LikeActionController.this.socialSentenceWithLike; String socialSentenceWithoutLike = result.containsKey(ShareConstants.EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE) ? result.getString( ShareConstants.EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE) : LikeActionController.this.socialSentenceWithoutLike; String unlikeToken = result.containsKey(ShareConstants.EXTRA_UNLIKE_TOKEN) ? result.getString(ShareConstants.EXTRA_UNLIKE_TOKEN) : LikeActionController.this.unlikeToken; updateState( objectIsLiked, likeCountWithLike, likeCountWithoutLike, socialSentenceWithLike, socialSentenceWithoutLike, unlikeToken); } }; likeStatusClient.setCompletedListener(callback); } private void publishAgainIfNeeded(final Bundle analyticsParameters) { if (isObjectLiked != isObjectLikedOnServer && !publishLikeOrUnlikeAsync(isObjectLiked, analyticsParameters)) { // Unable to re-publish the new desired state. Signal that there is an error and // revert the like state back. publishDidError(!isObjectLiked); } } private void fetchVerifiedObjectId(final RequestCompletionCallback completionHandler) { if (!Utility.isNullOrEmpty(verifiedObjectId)) { if (completionHandler != null) { completionHandler.onComplete(); } return; } final GetOGObjectIdRequestWrapper objectIdRequest = new GetOGObjectIdRequestWrapper(objectId, objectType); final GetPageIdRequestWrapper pageIdRequest = new GetPageIdRequestWrapper(objectId, objectType); GraphRequestBatch requestBatch = new GraphRequestBatch(); objectIdRequest.addToBatch(requestBatch); pageIdRequest.addToBatch(requestBatch); requestBatch.addCallback(new GraphRequestBatch.Callback() { @Override public void onBatchCompleted(GraphRequestBatch batch) { verifiedObjectId = objectIdRequest.verifiedObjectId; if (Utility.isNullOrEmpty(verifiedObjectId)) { verifiedObjectId = pageIdRequest.verifiedObjectId; objectIsPage = pageIdRequest.objectIsPage; } if (Utility.isNullOrEmpty(verifiedObjectId)) { Logger.log(LoggingBehavior.DEVELOPER_ERRORS, TAG, "Unable to verify the FB id for '%s'. Verify that it is a valid FB" + " object or page", objectId); logAppEventForError("get_verified_id", pageIdRequest.error != null ? pageIdRequest.error : objectIdRequest.error); } if (completionHandler != null) { completionHandler.onComplete(); } } }); requestBatch.executeAsync(); } private void logAppEventForError(String action, Bundle parameters) { Bundle logParams = new Bundle(parameters); logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_ID, objectId); logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_TYPE, objectType.toString()); logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_CURRENT_ACTION, action); getAppEventsLogger().logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_ERROR, null, logParams); } private void logAppEventForError(String action, FacebookRequestError error) { Bundle logParams = new Bundle(); if (error != null) { JSONObject requestResult = error.getRequestResult(); if (requestResult != null) { logParams.putString( AnalyticsEvents.PARAMETER_LIKE_VIEW_ERROR_JSON, requestResult.toString()); } } logAppEventForError(action, logParams); } // // Interfaces // /** * Used by the call to getControllerForObjectId() */ public interface CreationCallback { public void onComplete( LikeActionController likeActionController, FacebookException error); } /** * Used by all the request wrappers */ private interface RequestCompletionCallback { void onComplete(); } // // Inner classes // private class GetOGObjectIdRequestWrapper extends AbstractRequestWrapper { String verifiedObjectId; GetOGObjectIdRequestWrapper(String objectId, LikeView.ObjectType objectType) { super(objectId, objectType); Bundle objectIdRequestParams = new Bundle(); objectIdRequestParams.putString("fields", "og_object.fields(id)"); objectIdRequestParams.putString("ids", objectId); setRequest(new GraphRequest( AccessToken.getCurrentAccessToken(), "", objectIdRequestParams, HttpMethod.GET)); } @Override protected void processError(FacebookRequestError error) { // If this object Id is for a Page, an error will be received for this request // We will then rely on the other request to come through. if (error.getErrorMessage().contains("og_object")) { this.error = null; } else { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error getting the FB id for object '%s' with type '%s' : %s", objectId, objectType, error); } } @Override protected void processSuccess(GraphResponse response) { JSONObject results = Utility.tryGetJSONObjectFromResponse( response.getJSONObject(), objectId); if (results != null) { // See if we can get the OG object Id out JSONObject ogObject = results.optJSONObject("og_object"); if (ogObject != null) { verifiedObjectId = ogObject.optString("id"); } } } } private class GetPageIdRequestWrapper extends AbstractRequestWrapper { String verifiedObjectId; boolean objectIsPage; GetPageIdRequestWrapper(String objectId, LikeView.ObjectType objectType) { super(objectId, objectType); Bundle pageIdRequestParams = new Bundle(); pageIdRequestParams.putString("fields", "id"); pageIdRequestParams.putString("ids", objectId); setRequest(new GraphRequest( AccessToken.getCurrentAccessToken(), "", pageIdRequestParams, HttpMethod.GET)); } @Override protected void processSuccess(GraphResponse response) { JSONObject results = Utility.tryGetJSONObjectFromResponse( response.getJSONObject(), objectId); if (results != null) { verifiedObjectId = results.optString("id"); objectIsPage = !Utility.isNullOrEmpty(verifiedObjectId); } } @Override protected void processError(FacebookRequestError error) { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error getting the FB id for object '%s' with type '%s' : %s", objectId, objectType, error); } } private class PublishLikeRequestWrapper extends AbstractRequestWrapper { String unlikeToken; PublishLikeRequestWrapper(String objectId, LikeView.ObjectType objectType) { super(objectId, objectType); Bundle likeRequestParams = new Bundle(); likeRequestParams.putString("object", objectId); setRequest(new GraphRequest( AccessToken.getCurrentAccessToken(), "me/og.likes", likeRequestParams, HttpMethod.POST)); } @Override protected void processSuccess(GraphResponse response) { unlikeToken = Utility.safeGetStringFromResponse(response.getJSONObject(), "id"); } @Override protected void processError(FacebookRequestError error) { int errorCode = error.getErrorCode(); if (errorCode == ERROR_CODE_OBJECT_ALREADY_LIKED) { // This isn't an error for us. Client was just out of sync with server // This will prevent us from showing the dialog for this. // However, there is no unliketoken. So a subsequent unlike WILL show the dialog this.error = null; } else { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error liking object '%s' with type '%s' : %s", objectId, objectType, error); logAppEventForError("publish_like", error); } } } private class PublishUnlikeRequestWrapper extends AbstractRequestWrapper { private String unlikeToken; PublishUnlikeRequestWrapper(String unlikeToken) { super(null, null); this.unlikeToken = unlikeToken; setRequest(new GraphRequest( AccessToken.getCurrentAccessToken(), unlikeToken, null, HttpMethod.DELETE)); } @Override protected void processSuccess(GraphResponse response) { } @Override protected void processError(FacebookRequestError error) { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error unliking object with unlike token '%s' : %s", unlikeToken, error); logAppEventForError("publish_unlike", error); } } private class GetOGObjectLikesRequestWrapper extends AbstractRequestWrapper { // Initialize the like status to what we currently have. This way, empty/error responses // don't end up clearing out the state. boolean objectIsLiked = LikeActionController.this.isObjectLiked; String unlikeToken; GetOGObjectLikesRequestWrapper(String objectId, LikeView.ObjectType objectType) { super(objectId, objectType); Bundle requestParams = new Bundle(); requestParams.putString("fields", "id,application"); requestParams.putString("object", objectId); setRequest(new GraphRequest( AccessToken.getCurrentAccessToken(), "me/og.likes", requestParams, HttpMethod.GET)); } @Override protected void processSuccess(GraphResponse response) { JSONArray dataSet = Utility.tryGetJSONArrayFromResponse( response.getJSONObject(), "data"); if (dataSet != null) { for (int i = 0; i < dataSet.length(); i++) { JSONObject data = dataSet.optJSONObject(i); if (data != null) { objectIsLiked = true; JSONObject appData = data.optJSONObject("application"); AccessToken accessToken = AccessToken.getCurrentAccessToken(); if (appData != null && accessToken != null && Utility.areObjectsEqual( accessToken.getApplicationId(), appData.optString("id"))) { unlikeToken = data.optString("id"); } } } } } @Override protected void processError(FacebookRequestError error) { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error fetching like status for object '%s' with type '%s' : %s", objectId, objectType, error); logAppEventForError("get_og_object_like", error); } } private class GetEngagementRequestWrapper extends AbstractRequestWrapper { // Initialize the like status to what we currently have. This way, empty/error responses // don't end up clearing out the state. String likeCountStringWithLike = LikeActionController.this.likeCountStringWithLike; String likeCountStringWithoutLike = LikeActionController.this.likeCountStringWithoutLike; String socialSentenceStringWithLike = LikeActionController.this.socialSentenceWithLike; String socialSentenceStringWithoutLike = LikeActionController.this.socialSentenceWithoutLike; GetEngagementRequestWrapper(String objectId, LikeView.ObjectType objectType) { super(objectId, objectType); Bundle requestParams = new Bundle(); requestParams.putString( "fields", "engagement.fields(" + "count_string_with_like," + "count_string_without_like," + "social_sentence_with_like," + "social_sentence_without_like)"); setRequest(new GraphRequest( AccessToken.getCurrentAccessToken(), objectId, requestParams, HttpMethod.GET)); } @Override protected void processSuccess(GraphResponse response) { JSONObject engagementResults = Utility.tryGetJSONObjectFromResponse( response.getJSONObject(), "engagement"); if (engagementResults != null) { // Missing properties in the response should default to cached like status likeCountStringWithLike = engagementResults.optString( "count_string_with_like", likeCountStringWithLike); likeCountStringWithoutLike = engagementResults.optString( "count_string_without_like", likeCountStringWithoutLike); socialSentenceStringWithLike = engagementResults.optString( "social_sentence_with_like", socialSentenceStringWithLike); socialSentenceStringWithoutLike = engagementResults.optString( "social_sentence_without_like", socialSentenceStringWithoutLike); } } @Override protected void processError(FacebookRequestError error) { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error fetching engagement for object '%s' with type '%s' : %s", objectId, objectType, error); logAppEventForError("get_engagement", error); } } private abstract class AbstractRequestWrapper { private GraphRequest request; protected String objectId; protected LikeView.ObjectType objectType; FacebookRequestError error; protected AbstractRequestWrapper(String objectId, LikeView.ObjectType objectType) { this.objectId = objectId; this.objectType = objectType; } void addToBatch(GraphRequestBatch batch) { batch.add(request); } protected void setRequest(GraphRequest request) { this.request = request; // Make sure that our requests are hitting the latest version of the API known to this // sdk. request.setVersion(ServerProtocol.GRAPH_API_VERSION); request.setCallback(new GraphRequest.Callback() { @Override public void onCompleted(GraphResponse response) { error = response.getError(); if (error != null) { processError(error); } else { processSuccess(response); } } }); } protected void processError(FacebookRequestError error) { Logger.log(LoggingBehavior.REQUESTS, TAG, "Error running request for object '%s' with type '%s' : %s", objectId, objectType, error); } protected abstract void processSuccess(GraphResponse response); } // Performs cache re-ordering/trimming to keep most-recently-used items up front // ** NOTE ** It is expected that only _ONE_ MRUCacheWorkItem is ever running. This is enforced // by setting the concurrency of the WorkQueue to 1. Changing the concurrency will most likely // lead to errors. private static class MRUCacheWorkItem implements Runnable { private static ArrayList<String> mruCachedItems = new ArrayList<String>(); private String cacheItem; private boolean shouldTrim; MRUCacheWorkItem(String cacheItem, boolean shouldTrim) { this.cacheItem = cacheItem; this.shouldTrim = shouldTrim; } @Override public void run() { if (cacheItem != null) { mruCachedItems.remove(cacheItem); mruCachedItems.add(0, cacheItem); } if (shouldTrim && mruCachedItems.size() >= MAX_CACHE_SIZE) { int targetSize = MAX_CACHE_SIZE / 2; // Optimize for fewer trim-passes. while (targetSize < mruCachedItems.size()) { String cacheKey = mruCachedItems.remove(mruCachedItems.size() - 1); // Here is where we actually remove from the cache of LikeActionControllers. cache.remove(cacheKey); } } } } private static class SerializeToDiskWorkItem implements Runnable { private String cacheKey; private String controllerJson; SerializeToDiskWorkItem(String cacheKey, String controllerJson) { this.cacheKey = cacheKey; this.controllerJson = controllerJson; } @Override public void run() { serializeToDiskSynchronously(cacheKey, controllerJson); } } private static class CreateLikeActionControllerWorkItem implements Runnable { private String objectId; private LikeView.ObjectType objectType; private CreationCallback callback; CreateLikeActionControllerWorkItem( String objectId, LikeView.ObjectType objectType, CreationCallback callback) { this.objectId = objectId; this.objectType = objectType; this.callback = callback; } @Override public void run() { createControllerForObjectIdAndType(objectId, objectType, callback); } } }