Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14792 manas 1
/**
2
 * Copyright 2010-present Facebook.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *    http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.facebook.internal;
18
 
19
import android.app.Activity;
20
import android.content.*;
21
import android.os.Bundle;
22
import android.os.Handler;
23
import android.os.Looper;
24
import android.support.v4.content.LocalBroadcastManager;
25
import android.util.Log;
26
import com.facebook.*;
27
import com.facebook.widget.FacebookDialog;
28
import org.json.JSONArray;
29
import org.json.JSONException;
30
import org.json.JSONObject;
31
 
32
import java.io.IOException;
33
import java.io.InputStream;
34
import java.io.OutputStream;
35
import java.util.ArrayList;
36
import java.util.EnumSet;
37
import java.util.UUID;
38
import java.util.concurrent.ConcurrentHashMap;
39
 
40
/**
41
 * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of
42
 * any of the classes in this package is unsupported, and they may be modified or removed without warning at
43
 * any time.
44
 */
45
public class LikeActionController {
46
 
47
    public static final String ACTION_LIKE_ACTION_CONTROLLER_UPDATED = "com.facebook.sdk.LikeActionController.UPDATED";
48
    public static final String ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR = "com.facebook.sdk.LikeActionController.DID_ERROR";
49
    public static final String ACTION_LIKE_ACTION_CONTROLLER_DID_RESET = "com.facebook.sdk.LikeActionController.DID_RESET";
50
 
51
    public static final String ACTION_OBJECT_ID_KEY = "com.facebook.sdk.LikeActionController.OBJECT_ID";
52
 
53
    public static final String ERROR_INVALID_OBJECT_ID = "Invalid Object Id";
54
 
55
    private static final String TAG = LikeActionController.class.getSimpleName();
56
 
57
    private static final int LIKE_ACTION_CONTROLLER_VERSION = 2;
58
    private static final int MAX_CACHE_SIZE = 128;
59
    // MAX_OBJECT_SUFFIX basically accommodates for 1000 session-state changes before the async disk-cache-clear
60
    // finishes. The value is reasonably arbitrary.
61
    private static final int MAX_OBJECT_SUFFIX = 1000;
62
 
63
    private static final String LIKE_ACTION_CONTROLLER_STORE = "com.facebook.LikeActionController.CONTROLLER_STORE_KEY";
64
    private static final String LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY = "PENDING_CONTROLLER_KEY";
65
    private static final String LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY = "OBJECT_SUFFIX";
66
 
67
    private static final String JSON_INT_VERSION_KEY = "com.facebook.internal.LikeActionController.version";
68
    private static final String JSON_STRING_OBJECT_ID_KEY = "object_id";
69
    private static final String JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY = "like_count_string_with_like";
70
    private static final String JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY = "like_count_string_without_like";
71
    private static final String JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY = "social_sentence_with_like";
72
    private static final String JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY = "social_sentence_without_like";
73
    private static final String JSON_BOOL_IS_OBJECT_LIKED_KEY = "is_object_liked";
74
    private static final String JSON_STRING_UNLIKE_TOKEN_KEY = "unlike_token";
75
    private static final String JSON_STRING_PENDING_CALL_ID_KEY = "pending_call_id";
76
    private static final String JSON_BUNDLE_PENDING_CALL_ANALYTICS_BUNDLE = "pending_call_analytics_bundle";
77
 
78
    private static final String LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY = "object_is_liked";
79
    private static final String LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY = "like_count_string";
80
    private static final String LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY = "social_sentence";
81
    private static final String LIKE_DIALOG_RESPONSE_UNLIKE_TOKEN_KEY = "unlike_token";
82
 
83
    private static final int ERROR_CODE_OBJECT_ALREADY_LIKED = 3501;
84
 
85
    private static FileLruCache controllerDiskCache;
86
    private static final ConcurrentHashMap<String, LikeActionController> cache =
87
            new ConcurrentHashMap<String, LikeActionController>();
88
    private static WorkQueue mruCacheWorkQueue = new WorkQueue(1); // This MUST be 1 for proper synchronization
89
    private static WorkQueue diskIOWorkQueue = new WorkQueue(1); // This MUST be 1 for proper synchronization
90
    private static Handler handler;
91
    private static String objectIdForPendingController;
92
    private static boolean isPendingBroadcastReset;
93
    private static boolean isInitialized;
94
    private static volatile int objectSuffix;
95
 
96
    private Session session;
97
    private Context context;
98
    private String objectId;
99
    private boolean isObjectLiked;
100
    private String likeCountStringWithLike;
101
    private String likeCountStringWithoutLike;
102
    private String socialSentenceWithLike;
103
    private String socialSentenceWithoutLike;
104
    private String unlikeToken;
105
 
106
    private String verifiedObjectId;
107
    private boolean objectIsPage;
108
    private boolean isObjectLikedOnServer;
109
 
110
    private boolean isPendingLikeOrUnlike;
111
 
112
    private UUID pendingCallId;
113
 
114
    private Bundle pendingCallAnalyticsBundle;
115
 
116
    private AppEventsLogger appEventsLogger;
117
 
118
    /**
119
     * Called from UiLifecycleHelper to process any pending likes that had resulted in the Like dialog
120
     * being displayed
121
     *
122
     * @param context Hosting context
123
     * @param requestCode From the originating call to onActivityResult
124
     * @param resultCode From the originating call to onActivityResult
125
     * @param data From the originating call to onActivityResult
126
     * @return Indication of whether the Intent was handled
127
     */
128
    public static boolean handleOnActivityResult(Context context,
129
                                                 final int requestCode,
130
                                                 final int resultCode,
131
                                                 final Intent data) {
132
        final UUID callId = NativeProtocol.getCallIdFromIntent(data);
133
        if (callId == null) {
134
            return false;
135
        }
136
 
137
        // See if we were waiting on a Like dialog completion.
138
        if (Utility.isNullOrEmpty(objectIdForPendingController)) {
139
            SharedPreferences sharedPreferences = context.getSharedPreferences(
140
                    LIKE_ACTION_CONTROLLER_STORE,
141
                    Context.MODE_PRIVATE);
142
 
143
            objectIdForPendingController = sharedPreferences.getString(
144
                    LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY,
145
                    null);
146
        }
147
 
148
        if (Utility.isNullOrEmpty(objectIdForPendingController)) {
149
            // Doesn't look like we were waiting on a Like dialog completion
150
            return false;
151
        }
152
 
153
        getControllerForObjectId(
154
                context,
155
                objectIdForPendingController,
156
                new CreationCallback() {
157
                    @Override
158
                    public void onComplete(LikeActionController likeActionController) {
159
                        likeActionController.onActivityResult(requestCode, resultCode, data, callId);
160
                    }
161
                });
162
 
163
        return true;
164
    }
165
 
166
    /**
167
     * Called by the LikeView when an object-id is set on it.
168
     * @param context context
169
     * @param objectId Object Id
170
     * @return A LikeActionController for the specified object id
171
     */
172
    public static void getControllerForObjectId(
173
            Context context,
174
            String objectId,
175
            CreationCallback callback) {
176
        if (!isInitialized) {
177
            performFirstInitialize(context);
178
        }
179
 
180
        LikeActionController controllerForObject = getControllerFromInMemoryCache(objectId);
181
        if (controllerForObject != null) {
182
            // Direct object-cache hit
183
            invokeCallbackWithController(callback, controllerForObject);
184
        } else {
185
            diskIOWorkQueue.addActiveWorkItem(new CreateLikeActionControllerWorkItem(context, objectId, callback));
186
        }
187
    }
188
 
189
    /**
190
     * NOTE: This MUST be called ONLY via the CreateLikeActionControllerWorkItem class to ensure that it happens on the
191
     * right thread, at the right time.
192
     */
193
    private static void createControllerForObjectId(
194
            Context context,
195
            String objectId,
196
            CreationCallback callback) {
197
        // Check again to see if the controller was created before attempting to deserialize/create one.
198
        // Need to check this in the case where multiple LikeViews are looking for a controller for the same object
199
        // and all got queued up to create one. We only want the first one to go through with the creation, and the
200
        // rest should get the same instance from the object-cache.
201
        LikeActionController controllerForObject = getControllerFromInMemoryCache(objectId);
202
        if (controllerForObject != null) {
203
            // Direct object-cache hit
204
            invokeCallbackWithController(callback, controllerForObject);
205
            return;
206
        }
207
 
208
        // Try deserialize from disk
209
        controllerForObject = deserializeFromDiskSynchronously(context, objectId);
210
 
211
        if (controllerForObject == null) {
212
            controllerForObject = new LikeActionController(context, Session.getActiveSession(), objectId);
213
            serializeToDiskAsync(controllerForObject);
214
        }
215
 
216
        // Update object-cache.
217
        putControllerInMemoryCache(objectId, controllerForObject);
218
 
219
        // Refresh the controller on the Main thread.
220
        final LikeActionController controllerToRefresh = controllerForObject;
221
        handler.post(new Runnable() {
222
            @Override
223
            public void run() {
224
                controllerToRefresh.refreshStatusAsync();
225
            }
226
        });
227
 
228
        invokeCallbackWithController(callback, controllerToRefresh);
229
    }
230
 
231
    private synchronized static void performFirstInitialize(Context context) {
232
        if (isInitialized) {
233
            return;
234
        }
235
 
236
        handler = new Handler(Looper.getMainLooper());
237
 
238
        SharedPreferences sharedPreferences = context.getSharedPreferences(
239
                LIKE_ACTION_CONTROLLER_STORE,
240
                Context.MODE_PRIVATE);
241
 
242
        objectSuffix = sharedPreferences.getInt(LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY, 1);
243
        controllerDiskCache = new FileLruCache(context, TAG, new FileLruCache.Limits());
244
 
245
        registerSessionBroadcastReceivers(context);
246
 
247
        isInitialized = true;
248
    }
249
 
250
    private static void invokeCallbackWithController(final CreationCallback callback, final LikeActionController controller) {
251
        if (callback == null) {
252
            return;
253
        }
254
 
255
        handler.post(new Runnable() {
256
            @Override
257
            public void run() {
258
                callback.onComplete(controller);
259
            }
260
        });
261
    }
262
 
263
    //
264
    // In-memory mru-caching code
265
    //
266
 
267
    private static void registerSessionBroadcastReceivers(Context context) {
268
        LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(context);
269
 
270
        IntentFilter filter = new IntentFilter();
271
        filter.addAction(Session.ACTION_ACTIVE_SESSION_UNSET);
272
        filter.addAction(Session.ACTION_ACTIVE_SESSION_CLOSED);
273
        filter.addAction(Session.ACTION_ACTIVE_SESSION_OPENED);
274
 
275
        broadcastManager.registerReceiver(new BroadcastReceiver() {
276
            @Override
277
            public void onReceive(Context receiverContext, Intent intent) {
278
                if (isPendingBroadcastReset) {
279
                    return;
280
                }
281
 
282
                String action = intent.getAction();
283
                final boolean shouldClearDisk =
284
                        Utility.areObjectsEqual(Session.ACTION_ACTIVE_SESSION_UNSET, action) ||
285
                                Utility.areObjectsEqual(Session.ACTION_ACTIVE_SESSION_CLOSED, action);
286
 
287
 
288
                isPendingBroadcastReset = true;
289
                // Delaying sending the broadcast to reset, because we might get many successive calls from Session
290
                // (to UNSET, SET & OPEN) and a delay would prevent excessive chatter.
291
                final Context broadcastContext = receiverContext;
292
                handler.postDelayed(new Runnable() {
293
                    @Override
294
                    public void run() {
295
                        // Bump up the objectSuffix so that we don't have a filename collision between a cache-clear and
296
                        // and a cache-read/write.
297
                        //
298
                        // NOTE: We know that onReceive() was called on the main thread. This means that even this code
299
                        // is running on the main thread, and therefore, there aren't synchronization issues with
300
                        // incrementing the objectSuffix and clearing the caches here.
301
                        if (shouldClearDisk) {
302
                            objectSuffix = (objectSuffix + 1) % MAX_OBJECT_SUFFIX;
303
                            broadcastContext.getSharedPreferences(LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE)
304
                                    .edit()
305
                                    .putInt(LIKE_ACTION_CONTROLLER_STORE_OBJECT_SUFFIX_KEY, objectSuffix)
306
                                    .apply();
307
 
308
                            // Only clearing the actual caches. The MRU index will self-clean with usage.
309
                            // Clearing the caches is necessary to prevent leaking like-state across sessions.
310
                            cache.clear();
311
                            controllerDiskCache.clearCache();
312
                        }
313
 
314
                        broadcastAction(broadcastContext, null, ACTION_LIKE_ACTION_CONTROLLER_DID_RESET);
315
                        isPendingBroadcastReset = false;
316
                    }
317
                }, 100);
318
            }
319
        }, filter);
320
    }
321
 
322
    private static void putControllerInMemoryCache(String objectId, LikeActionController controllerForObject) {
323
        String cacheKey = getCacheKeyForObjectId(objectId);
324
        // Move this object to the front. Also trim cache if necessary
325
        mruCacheWorkQueue.addActiveWorkItem(new MRUCacheWorkItem(cacheKey, true));
326
 
327
        cache.put(cacheKey, controllerForObject);
328
    }
329
 
330
    private static LikeActionController getControllerFromInMemoryCache(String objectId) {
331
        String cacheKey = getCacheKeyForObjectId(objectId);
332
 
333
        LikeActionController controller = cache.get(cacheKey);
334
        if (controller != null) {
335
            // Move this object to the front
336
            mruCacheWorkQueue.addActiveWorkItem(new MRUCacheWorkItem(cacheKey, false));
337
        }
338
 
339
        return controller;
340
    }
341
 
342
    //
343
    // Disk caching code
344
    //
345
 
346
    private static void serializeToDiskAsync(LikeActionController controller) {
347
        String controllerJson = serializeToJson(controller);
348
        String cacheKey = getCacheKeyForObjectId(controller.objectId);
349
 
350
        if (!Utility.isNullOrEmpty(controllerJson) && !Utility.isNullOrEmpty(cacheKey)) {
351
            diskIOWorkQueue.addActiveWorkItem(new SerializeToDiskWorkItem(cacheKey, controllerJson));
352
        }
353
    }
354
 
355
    /**
356
     * NOTE: This MUST be called ONLY via the SerializeToDiskWorkItem class to ensure that it happens on the
357
     * right thread, at the right time.
358
     */
359
    private static void serializeToDiskSynchronously(String cacheKey, String controllerJson) {
360
        OutputStream outputStream = null;
361
        try {
362
            outputStream = controllerDiskCache.openPutStream(cacheKey);
363
            outputStream.write(controllerJson.getBytes());
364
        } catch (IOException e) {
365
            Log.e(TAG, "Unable to serialize controller to disk", e);
366
        } finally {
367
            if (outputStream != null) {
368
                Utility.closeQuietly(outputStream);
369
            }
370
        }
371
    }
372
 
373
    /**
374
     * NOTE: This MUST be called ONLY via the CreateLikeActionControllerWorkItem class to ensure that it happens on the
375
     * right thread, at the right time.
376
     */
377
    private static LikeActionController deserializeFromDiskSynchronously(
378
            Context context,
379
            String objectId) {
380
        LikeActionController controller = null;
381
 
382
        InputStream inputStream = null;
383
        try {
384
            String cacheKey = getCacheKeyForObjectId(objectId);
385
            inputStream = controllerDiskCache.get(cacheKey);
386
            if (inputStream != null) {
387
                String controllerJsonString = Utility.readStreamToString(inputStream);
388
                if (!Utility.isNullOrEmpty(controllerJsonString)) {
389
                    controller = deserializeFromJson(context, controllerJsonString);
390
                }
391
            }
392
        } catch (IOException e) {
393
            Log.e(TAG, "Unable to deserialize controller from disk", e);
394
            controller = null;
395
        } finally {
396
            if (inputStream != null) {
397
                Utility.closeQuietly(inputStream);
398
            }
399
        }
400
 
401
        return controller;
402
    }
403
 
404
    private static LikeActionController deserializeFromJson(Context context, String controllerJsonString) {
405
        LikeActionController controller;
406
 
407
        try {
408
            JSONObject controllerJson = new JSONObject(controllerJsonString);
409
            int version = controllerJson.optInt(JSON_INT_VERSION_KEY, -1);
410
            if (version != LIKE_ACTION_CONTROLLER_VERSION) {
411
                // Don't attempt to deserialize a controller that might be serialized differently than expected.
412
                return null;
413
            }
414
 
415
            controller = new LikeActionController(
416
                    context,
417
                    Session.getActiveSession(),
418
                    controllerJson.getString(JSON_STRING_OBJECT_ID_KEY));
419
 
420
            // Make sure to default to null and not empty string, to keep the logic elsewhere functioning properly.
421
            controller.likeCountStringWithLike = controllerJson.optString(JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY, null) ;
422
            controller.likeCountStringWithoutLike = controllerJson.optString(JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY, null) ;
423
            controller.socialSentenceWithLike = controllerJson.optString(JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY, null);
424
            controller.socialSentenceWithoutLike = controllerJson.optString(JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY, null);
425
            controller.isObjectLiked = controllerJson.optBoolean(JSON_BOOL_IS_OBJECT_LIKED_KEY);
426
            controller.unlikeToken = controllerJson.optString(JSON_STRING_UNLIKE_TOKEN_KEY, null);
427
            String pendingCallIdString = controllerJson.optString(JSON_STRING_PENDING_CALL_ID_KEY, null);
428
            if (!Utility.isNullOrEmpty(pendingCallIdString)) {
429
                controller.pendingCallId = UUID.fromString(pendingCallIdString);
430
            }
431
 
432
            JSONObject analyticsJSON = controllerJson.optJSONObject(JSON_BUNDLE_PENDING_CALL_ANALYTICS_BUNDLE);
433
            if (analyticsJSON != null) {
434
                controller.pendingCallAnalyticsBundle = BundleJSONConverter.convertToBundle(analyticsJSON);
435
            }
436
        } catch (JSONException e) {
437
            Log.e(TAG, "Unable to deserialize controller from JSON", e);
438
            controller = null;
439
        }
440
 
441
        return controller;
442
    }
443
 
444
    private static String serializeToJson(LikeActionController controller) {
445
        JSONObject controllerJson = new JSONObject();
446
        try {
447
            controllerJson.put(JSON_INT_VERSION_KEY, LIKE_ACTION_CONTROLLER_VERSION);
448
            controllerJson.put(JSON_STRING_OBJECT_ID_KEY, controller.objectId);
449
            controllerJson.put(JSON_STRING_LIKE_COUNT_WITH_LIKE_KEY, controller.likeCountStringWithLike);
450
            controllerJson.put(JSON_STRING_LIKE_COUNT_WITHOUT_LIKE_KEY, controller.likeCountStringWithoutLike);
451
            controllerJson.put(JSON_STRING_SOCIAL_SENTENCE_WITH_LIKE_KEY, controller.socialSentenceWithLike);
452
            controllerJson.put(JSON_STRING_SOCIAL_SENTENCE_WITHOUT_LIKE_KEY, controller.socialSentenceWithoutLike);
453
            controllerJson.put(JSON_BOOL_IS_OBJECT_LIKED_KEY, controller.isObjectLiked);
454
            controllerJson.put(JSON_STRING_UNLIKE_TOKEN_KEY, controller.unlikeToken);
455
            if (controller.pendingCallId != null) {
456
                controllerJson.put(JSON_STRING_PENDING_CALL_ID_KEY, controller.pendingCallId.toString());
457
            }
458
            if (controller.pendingCallAnalyticsBundle != null) {
459
                JSONObject analyticsJSON = BundleJSONConverter.convertToJSON(controller.pendingCallAnalyticsBundle);
460
                if (analyticsJSON != null) {
461
                    controllerJson.put(JSON_BUNDLE_PENDING_CALL_ANALYTICS_BUNDLE, analyticsJSON);
462
                }
463
            }
464
        } catch (JSONException e) {
465
            Log.e(TAG, "Unable to serialize controller to JSON", e);
466
            return null;
467
        }
468
 
469
        return controllerJson.toString();
470
    }
471
 
472
    private static String getCacheKeyForObjectId(String objectId) {
473
        String accessTokenPortion = null;
474
        Session activeSession = Session.getActiveSession();
475
        if (activeSession != null && activeSession.isOpened()) {
476
            accessTokenPortion = activeSession.getAccessToken();
477
        }
478
        if (accessTokenPortion != null) {
479
            // Cache-key collisions are not something to worry about here, since we only store state for
480
            // one session. Even in the case where the previous session's serialized files have not been deleted yet,
481
            // the objectSuffix will be different due to the session-change, thus making the key different.
482
            accessTokenPortion = Utility.md5hash(accessTokenPortion);
483
        }
484
        return String.format(
485
                "%s|%s|com.fb.sdk.like|%d",
486
                objectId,
487
                Utility.coerceValueIfNullOrEmpty(accessTokenPortion, ""),
488
                objectSuffix);
489
    }
490
 
491
    //
492
    // Broadcast handling code
493
    //
494
 
495
    private static void broadcastAction(Context context, LikeActionController controller, String action) {
496
        broadcastAction(context, controller, action, null);
497
    }
498
 
499
    private static void broadcastAction(Context context, LikeActionController controller, String action, Bundle data) {
500
        Intent broadcastIntent = new Intent(action);
501
        if (controller != null) {
502
            if (data == null) {
503
                data = new Bundle();
504
            }
505
 
506
            data.putString(ACTION_OBJECT_ID_KEY, controller.getObjectId());
507
        }
508
 
509
        if (data != null) {
510
            broadcastIntent.putExtras(data);
511
        }
512
        LocalBroadcastManager.getInstance(context.getApplicationContext()).sendBroadcast(broadcastIntent);
513
    }
514
 
515
    /**
516
     * Constructor
517
     */
518
    private LikeActionController(Context context, Session session, String objectId) {
519
        this.context = context;
520
        this.session = session;
521
        this.objectId = objectId;
522
 
523
        appEventsLogger = AppEventsLogger.newLogger(context, session);
524
    }
525
 
526
    /**
527
     * Gets the the associated object id
528
     * @return object id
529
     */
530
    public String getObjectId() {
531
        return objectId;
532
    }
533
 
534
    /**
535
     * Gets the String representation of the like-count for the associated object
536
     * @return String representation of the like-count for the associated object
537
     */
538
    public String getLikeCountString() {
539
        return isObjectLiked ? likeCountStringWithLike : likeCountStringWithoutLike;
540
    }
541
 
542
    /**
543
     * Gets the String representation of the like-count for the associated object
544
     * @return String representation of the like-count for the associated object
545
     */
546
    public String getSocialSentence() {
547
        return isObjectLiked ? socialSentenceWithLike : socialSentenceWithoutLike;
548
    }
549
 
550
    /**
551
     * Indicates whether the associated object is liked
552
     * @return Indication of whether the associated object is liked
553
     */
554
    public boolean isObjectLiked() {
555
        return isObjectLiked;
556
    }
557
 
558
    /**
559
     * Entry-point to the code that performs the like/unlike action.
560
     */
561
    public void toggleLike(Activity activity, Bundle analyticsParameters) {
562
        appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_TAP, null, analyticsParameters);
563
 
564
        boolean shouldLikeObject = !this.isObjectLiked;
565
        if (canUseOGPublish(shouldLikeObject)) {
566
            // Update UI state optimistically
567
            updateState(shouldLikeObject,
568
                    this.likeCountStringWithLike,
569
                    this.likeCountStringWithoutLike,
570
                    this.socialSentenceWithLike,
571
                    this.socialSentenceWithoutLike,
572
                    this.unlikeToken);
573
            if (isPendingLikeOrUnlike) {
574
                // If the user toggled the button quickly, and there is still a publish underway, don't fire off
575
                // another request. Also log this behavior.
576
 
577
                appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_UNDO_QUICKLY, null, analyticsParameters);
578
                return;
579
            }
580
        }
581
 
582
        performLikeOrUnlike(activity, shouldLikeObject, analyticsParameters);
583
    }
584
 
585
    private void performLikeOrUnlike(Activity activity, boolean shouldLikeObject, Bundle analyticsParameters) {
586
        if (canUseOGPublish(shouldLikeObject)) {
587
            if (shouldLikeObject) {
588
                publishLikeAsync(activity, analyticsParameters);
589
            } else {
590
                publishUnlikeAsync(activity, analyticsParameters);
591
            }
592
        } else {
593
            presentLikeDialog(activity, analyticsParameters);
594
        }
595
    }
596
 
597
    private void updateState(boolean isObjectLiked,
598
                             String likeCountStringWithLike,
599
                             String likeCountStringWithoutLike,
600
                             String socialSentenceWithLike,
601
                             String socialSentenceWithoutLike,
602
                             String unlikeToken) {
603
        // Normalize all empty strings to null, so that we don't have any problems with comparison.
604
        likeCountStringWithLike = Utility.coerceValueIfNullOrEmpty(likeCountStringWithLike, null);
605
        likeCountStringWithoutLike = Utility.coerceValueIfNullOrEmpty(likeCountStringWithoutLike, null);
606
        socialSentenceWithLike = Utility.coerceValueIfNullOrEmpty(socialSentenceWithLike, null);
607
        socialSentenceWithoutLike = Utility.coerceValueIfNullOrEmpty(socialSentenceWithoutLike, null);
608
        unlikeToken = Utility.coerceValueIfNullOrEmpty(unlikeToken, null);
609
 
610
        boolean stateChanged = isObjectLiked != this.isObjectLiked ||
611
                !Utility.areObjectsEqual(likeCountStringWithLike, this.likeCountStringWithLike) ||
612
                !Utility.areObjectsEqual(likeCountStringWithoutLike, this.likeCountStringWithoutLike) ||
613
                !Utility.areObjectsEqual(socialSentenceWithLike, this.socialSentenceWithLike) ||
614
                !Utility.areObjectsEqual(socialSentenceWithoutLike, this.socialSentenceWithoutLike) ||
615
                !Utility.areObjectsEqual(unlikeToken, this.unlikeToken);
616
 
617
        if (!stateChanged) {
618
            return;
619
        }
620
 
621
        this.isObjectLiked = isObjectLiked;
622
        this.likeCountStringWithLike = likeCountStringWithLike;
623
        this.likeCountStringWithoutLike = likeCountStringWithoutLike;
624
        this.socialSentenceWithLike = socialSentenceWithLike;
625
        this.socialSentenceWithoutLike = socialSentenceWithoutLike;
626
        this.unlikeToken = unlikeToken;
627
 
628
        serializeToDiskAsync(this);
629
 
630
        broadcastAction(context, this, ACTION_LIKE_ACTION_CONTROLLER_UPDATED);
631
    }
632
 
633
    private void presentLikeDialog(Activity activity, Bundle analyticsParameters) {
634
        LikeDialogBuilder likeDialogBuilder = new LikeDialogBuilder(activity, objectId);
635
 
636
        if (likeDialogBuilder.canPresent()) {
637
            trackPendingCall(likeDialogBuilder.build().present(), analyticsParameters);
638
            appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_DIALOG, null, analyticsParameters);
639
        } else {
640
            String webFallbackUrl = likeDialogBuilder.getWebFallbackUrl();
641
            if (!Utility.isNullOrEmpty(webFallbackUrl)) {
642
                boolean webFallbackShown = FacebookWebFallbackDialog.presentWebFallback(
643
                        activity,
644
                        webFallbackUrl,
645
                        likeDialogBuilder.getApplicationId(),
646
                        likeDialogBuilder.getAppCall(),
647
                        getFacebookDialogCallback(analyticsParameters));
648
                if (webFallbackShown) {
649
                    appEventsLogger.logSdkEvent(
650
                            AnalyticsEvents.EVENT_LIKE_VIEW_DID_PRESENT_FALLBACK, null, analyticsParameters);
651
                }
652
            }
653
        }
654
    }
655
 
656
    private boolean onActivityResult(int requestCode, int resultCode, Intent data, UUID callId) {
657
        if (pendingCallId == null || !pendingCallId.equals(callId)) {
658
            return false;
659
        }
660
 
661
        // See if we were waiting for a dialog completion
662
        FacebookDialog.PendingCall pendingCall = PendingCallStore.getInstance().getPendingCallById(pendingCallId);
663
        if (pendingCall == null) {
664
            return false;
665
        }
666
 
667
        // Look for results
668
        FacebookDialog.handleActivityResult(
669
                context,
670
                pendingCall,
671
                requestCode,
672
                data,
673
                getFacebookDialogCallback(pendingCallAnalyticsBundle));
674
 
675
        // The handlers from above will run synchronously. So by the time we get here, it should be safe to
676
        // stop tracking this call and also serialize the controller to disk
677
        stopTrackingPendingCall();
678
 
679
        return true;
680
    }
681
 
682
    private FacebookDialog.Callback getFacebookDialogCallback(final Bundle analyticsParameters) {
683
        return new FacebookDialog.Callback() {
684
            @Override
685
            public void onComplete(FacebookDialog.PendingCall pendingCall, Bundle data) {
686
                if (data == null || !data.containsKey(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY)) {
687
                    // This is an empty result that we can't handle.
688
                    return;
689
                }
690
 
691
                boolean isObjectLiked = data.getBoolean(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY);
692
 
693
                // Default to known/cached state, if properties are missing.
694
                String likeCountStringWithLike = LikeActionController.this.likeCountStringWithLike;
695
                String likeCountStringWithoutLike = LikeActionController.this.likeCountStringWithoutLike;
696
                if (data.containsKey(LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY)) {
697
                    likeCountStringWithLike = data.getString(LIKE_DIALOG_RESPONSE_LIKE_COUNT_STRING_KEY);
698
                    likeCountStringWithoutLike = likeCountStringWithLike;
699
                }
700
 
701
                String socialSentenceWithLike = LikeActionController.this.socialSentenceWithLike;
702
                String socialSentenceWithoutWithoutLike = LikeActionController.this.socialSentenceWithoutLike;
703
                if (data.containsKey(LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY)) {
704
                    socialSentenceWithLike = data.getString(LIKE_DIALOG_RESPONSE_SOCIAL_SENTENCE_KEY);
705
                    socialSentenceWithoutWithoutLike = socialSentenceWithLike;
706
                }
707
 
708
                String unlikeToken = data.containsKey(LIKE_DIALOG_RESPONSE_OBJECT_IS_LIKED_KEY)
709
                        ? data.getString(LIKE_DIALOG_RESPONSE_UNLIKE_TOKEN_KEY)
710
                        : LikeActionController.this.unlikeToken;
711
 
712
                Bundle logParams = (analyticsParameters == null) ? new Bundle() : analyticsParameters;
713
                logParams.putString(AnalyticsEvents.PARAMETER_CALL_ID, pendingCall.getCallId().toString());
714
                appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DIALOG_DID_SUCCEED, null, logParams);
715
 
716
                updateState(
717
                        isObjectLiked,
718
                        likeCountStringWithLike,
719
                        likeCountStringWithoutLike,
720
                        socialSentenceWithLike,
721
                        socialSentenceWithoutWithoutLike,
722
                        unlikeToken);
723
            }
724
 
725
            @Override
726
            public void onError(FacebookDialog.PendingCall pendingCall, Exception error, Bundle data) {
727
                Logger.log(LoggingBehavior.REQUESTS, TAG, "Like Dialog failed with error : %s", error);
728
 
729
                Bundle logParams = analyticsParameters == null ? new Bundle() : analyticsParameters;
730
                logParams.putString(AnalyticsEvents.PARAMETER_CALL_ID, pendingCall.getCallId().toString());
731
 
732
                // Log the error and AppEvent
733
                logAppEventForError("present_dialog", logParams);
734
 
735
                broadcastAction(context, LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, data);
736
            }
737
        };
738
    }
739
 
740
    private void trackPendingCall(FacebookDialog.PendingCall pendingCall, Bundle analyticsParameters) {
741
        PendingCallStore.getInstance().trackPendingCall(pendingCall);
742
 
743
        // Save off the call id for processing the response
744
        pendingCallId = pendingCall.getCallId();
745
        storeObjectIdForPendingController(objectId);
746
 
747
        // Store off the analytics parameters as well, for completion-logging
748
        pendingCallAnalyticsBundle = analyticsParameters;
749
 
750
        // Serialize to disk, in case we get terminated while waiting for the dialog to complete
751
        serializeToDiskAsync(this);
752
    }
753
 
754
    private void stopTrackingPendingCall() {
755
        PendingCallStore.getInstance().stopTrackingPendingCall(pendingCallId);
756
 
757
        pendingCallId = null;
758
        pendingCallAnalyticsBundle = null;
759
 
760
        storeObjectIdForPendingController(null);
761
    }
762
 
763
    private void storeObjectIdForPendingController(String objectId) {
764
        objectIdForPendingController = objectId;
765
        context.getSharedPreferences(LIKE_ACTION_CONTROLLER_STORE, Context.MODE_PRIVATE)
766
                .edit()
767
                .putString(LIKE_ACTION_CONTROLLER_STORE_PENDING_OBJECT_ID_KEY, objectIdForPendingController)
768
                .apply();
769
    }
770
 
771
    private boolean canUseOGPublish(boolean willPerformLike) {
772
        // Verify that the object isn't a Page, that we have permissions and that, if we're unliking, then
773
        // we have an unlike token.
774
        return !objectIsPage &&
775
                verifiedObjectId != null &&
776
                session != null &&
777
                session.getPermissions() != null &&
778
                session.getPermissions().contains("publish_actions") &&
779
                (willPerformLike || !Utility.isNullOrEmpty(unlikeToken));
780
    }
781
 
782
    private void publishLikeAsync(final Activity activity, final Bundle analyticsParameters) {
783
        isPendingLikeOrUnlike = true;
784
 
785
        fetchVerifiedObjectId(new RequestCompletionCallback() {
786
            @Override
787
            public void onComplete() {
788
                if (Utility.isNullOrEmpty(verifiedObjectId)) {
789
                    // Could not get a verified id
790
                    Bundle errorBundle = new Bundle();
791
                    errorBundle.putString(NativeProtocol.STATUS_ERROR_DESCRIPTION, ERROR_INVALID_OBJECT_ID);
792
 
793
                    broadcastAction(context, LikeActionController.this, ACTION_LIKE_ACTION_CONTROLLER_DID_ERROR, errorBundle);
794
                    return;
795
                }
796
 
797
                // Perform the Like.
798
                RequestBatch requestBatch = new RequestBatch();
799
                final PublishLikeRequestWrapper likeRequest = new PublishLikeRequestWrapper(verifiedObjectId);
800
                likeRequest.addToBatch(requestBatch);
801
                requestBatch.addCallback(new RequestBatch.Callback() {
802
                    @Override
803
                    public void onBatchCompleted(RequestBatch batch) {
804
                        isPendingLikeOrUnlike = false;
805
 
806
                        if (likeRequest.error != null) {
807
                            // We already updated the UI to show button in the Liked state. Since this failed, let's
808
                            // revert back to the Unliked state and show the dialog. We need to do this because the
809
                            // dialog-flow expects the button to only be updated once the dialog returns
810
 
811
                            updateState(
812
                                    false,
813
                                    likeCountStringWithLike,
814
                                    likeCountStringWithoutLike,
815
                                    socialSentenceWithLike,
816
                                    socialSentenceWithoutLike,
817
                                    unlikeToken);
818
 
819
                            presentLikeDialog(activity, analyticsParameters);
820
                        } else {
821
                            unlikeToken = Utility.coerceValueIfNullOrEmpty(likeRequest.unlikeToken, null);
822
                            isObjectLikedOnServer = true;
823
 
824
                            appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_LIKE, null, analyticsParameters);
825
 
826
                            toggleAgainIfNeeded(activity, analyticsParameters);
827
                        }
828
                    }
829
                });
830
 
831
                requestBatch.executeAsync();
832
            }
833
        });
834
    }
835
 
836
    private void publishUnlikeAsync(final Activity activity, final Bundle analyticsParameters) {
837
        isPendingLikeOrUnlike = true;
838
 
839
        // Perform the Unlike.
840
        RequestBatch requestBatch = new RequestBatch();
841
        final PublishUnlikeRequestWrapper unlikeRequest = new PublishUnlikeRequestWrapper(unlikeToken);
842
        unlikeRequest.addToBatch(requestBatch);
843
        requestBatch.addCallback(new RequestBatch.Callback() {
844
            @Override
845
            public void onBatchCompleted(RequestBatch batch) {
846
                isPendingLikeOrUnlike = false;
847
 
848
                if (unlikeRequest.error != null) {
849
                    // We already updated the UI to show button in the Unliked state. Since this failed, let's
850
                    // revert back to the Liked state and show the dialog. We need to do this because the
851
                    // dialog-flow expects the button to only be updated once the dialog returns
852
 
853
                    updateState(
854
                            true,
855
                            likeCountStringWithLike,
856
                            likeCountStringWithoutLike,
857
                            socialSentenceWithLike,
858
                            socialSentenceWithoutLike,
859
                            unlikeToken);
860
 
861
                    presentLikeDialog(activity, analyticsParameters);
862
                } else {
863
                    unlikeToken = null;
864
                    isObjectLikedOnServer = false;
865
 
866
                    appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_DID_UNLIKE, null, analyticsParameters);
867
 
868
                    toggleAgainIfNeeded(activity, analyticsParameters);
869
                }
870
            }
871
        });
872
 
873
        requestBatch.executeAsync();
874
    }
875
 
876
    private void refreshStatusAsync() {
877
        if (session == null || session.isClosed() || SessionState.CREATED.equals(session.getState())) {
878
            // Only when we know that there is no active session, or if there is, it is not open OR being opened,
879
            // should we attempt getting like state from the service. Otherwise, use the access token of the session
880
            // to make sure we get the correct like state.
881
            refreshStatusViaService();
882
            return;
883
        }
884
 
885
        fetchVerifiedObjectId(new RequestCompletionCallback() {
886
            @Override
887
            public void onComplete() {
888
                final GetOGObjectLikesRequestWrapper objectLikesRequest =
889
                        new GetOGObjectLikesRequestWrapper(verifiedObjectId);
890
                final GetEngagementRequestWrapper engagementRequest =
891
                        new GetEngagementRequestWrapper(verifiedObjectId);
892
 
893
                RequestBatch requestBatch = new RequestBatch();
894
                objectLikesRequest.addToBatch(requestBatch);
895
                engagementRequest.addToBatch(requestBatch);
896
 
897
                requestBatch.addCallback(new RequestBatch.Callback() {
898
                    @Override
899
                    public void onBatchCompleted(RequestBatch batch) {
900
                        if (objectLikesRequest.error != null ||
901
                                engagementRequest.error != null) {
902
                            // Refreshing is best-effort. If the refresh fails, don't lose old state.
903
                            Logger.log(
904
                                    LoggingBehavior.REQUESTS,
905
                                    TAG,
906
                                    "Unable to refresh like state for id: '%s'", objectId);
907
                            return;
908
                        }
909
 
910
                        updateState(
911
                                objectLikesRequest.objectIsLiked,
912
                                engagementRequest.likeCountStringWithLike,
913
                                engagementRequest.likeCountStringWithoutLike,
914
                                engagementRequest.socialSentenceStringWithLike,
915
                                engagementRequest.socialSentenceStringWithoutLike,
916
                                objectLikesRequest.unlikeToken);
917
                    }
918
                });
919
 
920
                requestBatch.executeAsync();
921
            }
922
        });
923
    }
924
 
925
    private void refreshStatusViaService() {
926
        LikeStatusClient likeStatusClient = new LikeStatusClient(
927
                context,
928
                Settings.getApplicationId(),
929
                objectId);
930
        if (!likeStatusClient.start()) {
931
            return;
932
        }
933
 
934
        LikeStatusClient.CompletedListener callback = new LikeStatusClient.CompletedListener() {
935
            @Override
936
            public void completed(Bundle result) {
937
                // Don't lose old state if the service response is incomplete.
938
                if (result == null || !result.containsKey(NativeProtocol.EXTRA_OBJECT_IS_LIKED)) {
939
                    return;
940
                }
941
 
942
                boolean objectIsLiked = result.getBoolean(NativeProtocol.EXTRA_OBJECT_IS_LIKED);
943
 
944
                String likeCountWithLike =
945
                        result.containsKey(NativeProtocol.EXTRA_LIKE_COUNT_STRING_WITH_LIKE)
946
                                ? result.getString(NativeProtocol.EXTRA_LIKE_COUNT_STRING_WITH_LIKE)
947
                                : LikeActionController.this.likeCountStringWithLike;
948
 
949
                String likeCountWithoutLike =
950
                        result.containsKey(NativeProtocol.EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE)
951
                                ? result.getString(NativeProtocol.EXTRA_LIKE_COUNT_STRING_WITHOUT_LIKE)
952
                                : LikeActionController.this.likeCountStringWithoutLike;
953
 
954
                String socialSentenceWithLike =
955
                        result.containsKey(NativeProtocol.EXTRA_SOCIAL_SENTENCE_WITH_LIKE)
956
                                ? result.getString(NativeProtocol.EXTRA_SOCIAL_SENTENCE_WITH_LIKE)
957
                                : LikeActionController.this.socialSentenceWithLike;
958
 
959
                String socialSentenceWithoutLike =
960
                        result.containsKey(NativeProtocol.EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE)
961
                                ? result.getString(NativeProtocol.EXTRA_SOCIAL_SENTENCE_WITHOUT_LIKE)
962
                                : LikeActionController.this.socialSentenceWithoutLike;
963
 
964
                String unlikeToken =
965
                        result.containsKey(NativeProtocol.EXTRA_UNLIKE_TOKEN)
966
                                ? result.getString(NativeProtocol.EXTRA_UNLIKE_TOKEN)
967
                                : LikeActionController.this.unlikeToken;
968
 
969
                updateState(
970
                        objectIsLiked,
971
                        likeCountWithLike,
972
                        likeCountWithoutLike,
973
                        socialSentenceWithLike,
974
                        socialSentenceWithoutLike,
975
                        unlikeToken);
976
            }
977
        };
978
 
979
        likeStatusClient.setCompletedListener(callback);
980
    }
981
 
982
    private void toggleAgainIfNeeded(Activity activity, Bundle analyticsParameters) {
983
        if (isObjectLiked != isObjectLikedOnServer) {
984
            performLikeOrUnlike(activity, isObjectLiked, analyticsParameters);
985
        }
986
    }
987
 
988
    private void fetchVerifiedObjectId(final RequestCompletionCallback completionHandler) {
989
        if (!Utility.isNullOrEmpty(verifiedObjectId)) {
990
            if (completionHandler != null) {
991
                completionHandler.onComplete();
992
            }
993
 
994
            return;
995
        }
996
 
997
        final GetOGObjectIdRequestWrapper objectIdRequest = new GetOGObjectIdRequestWrapper(objectId);
998
        final GetPageIdRequestWrapper pageIdRequest = new GetPageIdRequestWrapper(objectId);
999
 
1000
        RequestBatch requestBatch = new RequestBatch();
1001
        objectIdRequest.addToBatch(requestBatch);
1002
        pageIdRequest.addToBatch(requestBatch);
1003
 
1004
        requestBatch.addCallback(new RequestBatch.Callback() {
1005
            @Override
1006
            public void onBatchCompleted(RequestBatch batch) {
1007
                verifiedObjectId = objectIdRequest.verifiedObjectId;
1008
                if (Utility.isNullOrEmpty(verifiedObjectId)) {
1009
                    verifiedObjectId = pageIdRequest.verifiedObjectId;
1010
                    objectIsPage = pageIdRequest.objectIsPage;
1011
                }
1012
 
1013
                if (Utility.isNullOrEmpty(verifiedObjectId)) {
1014
                    Logger.log(LoggingBehavior.DEVELOPER_ERRORS,
1015
                            TAG,
1016
                            "Unable to verify the FB id for '%s'. Verify that it is a valid FB object or page", objectId);
1017
                    logAppEventForError("get_verified_id",
1018
                            pageIdRequest.error != null ? pageIdRequest.error : objectIdRequest.error);
1019
                }
1020
 
1021
                if (completionHandler != null) {
1022
                    completionHandler.onComplete();
1023
                }
1024
            }
1025
        });
1026
 
1027
        requestBatch.executeAsync();
1028
    }
1029
 
1030
    private void logAppEventForError(String action, Bundle parameters) {
1031
        Bundle logParams = new Bundle(parameters);
1032
        logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_OBJECT_ID, objectId);
1033
        logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_CURRENT_ACTION, action);
1034
 
1035
        appEventsLogger.logSdkEvent(AnalyticsEvents.EVENT_LIKE_VIEW_ERROR, null, logParams);
1036
    }
1037
 
1038
    private void logAppEventForError(String action, FacebookRequestError error) {
1039
        Bundle logParams = new Bundle();
1040
        if (error != null) {
1041
            JSONObject requestResult = error.getRequestResult();
1042
            if (requestResult != null) {
1043
                logParams.putString(AnalyticsEvents.PARAMETER_LIKE_VIEW_ERROR_JSON, requestResult.toString());
1044
            }
1045
        }
1046
        logAppEventForError(action, logParams);
1047
    }
1048
 
1049
    //
1050
    // Interfaces
1051
    //
1052
 
1053
    /**
1054
     * Used by the call to getControllerForObjectId()
1055
     */
1056
    public interface CreationCallback {
1057
        public void onComplete(LikeActionController likeActionController);
1058
    }
1059
 
1060
    /**
1061
     * Used by all the request wrappers
1062
     */
1063
    private interface RequestCompletionCallback {
1064
        void onComplete();
1065
    }
1066
 
1067
    //
1068
    // Inner classes
1069
    //
1070
 
1071
    private class GetOGObjectIdRequestWrapper extends AbstractRequestWrapper {
1072
        String verifiedObjectId;
1073
 
1074
        GetOGObjectIdRequestWrapper(String objectId) {
1075
            super(objectId);
1076
 
1077
            Bundle objectIdRequestParams = new Bundle();
1078
            objectIdRequestParams.putString("fields", "og_object.fields(id)");
1079
            objectIdRequestParams.putString("ids", objectId);
1080
 
1081
            setRequest(new Request(session, "", objectIdRequestParams, HttpMethod.GET));
1082
        }
1083
 
1084
        @Override
1085
        protected void processError(FacebookRequestError error) {
1086
            // If this object Id is for a Page, an error will be received for this request
1087
            // We will then rely on the other request to come through.
1088
            if (error.getErrorMessage().contains("og_object")) {
1089
                this.error = null;
1090
            } else {
1091
                Logger.log(LoggingBehavior.REQUESTS,
1092
                        TAG,
1093
                        "Error getting the FB id for object '%s' : %s", objectId, error);
1094
            }
1095
        }
1096
 
1097
        @Override
1098
        protected void processSuccess(Response response) {
1099
            JSONObject results = Utility.tryGetJSONObjectFromResponse(response.getGraphObject(), objectId);
1100
            if (results != null) {
1101
                // See if we can get the OG object Id out
1102
                JSONObject ogObject = results.optJSONObject("og_object");
1103
                if (ogObject != null) {
1104
                    verifiedObjectId = ogObject.optString("id");
1105
                }
1106
            }
1107
        }
1108
    }
1109
 
1110
    private class GetPageIdRequestWrapper extends AbstractRequestWrapper {
1111
        String verifiedObjectId;
1112
        boolean objectIsPage;
1113
 
1114
        GetPageIdRequestWrapper(String objectId) {
1115
            super(objectId);
1116
 
1117
            Bundle pageIdRequestParams = new Bundle();
1118
            pageIdRequestParams.putString("fields", "id");
1119
            pageIdRequestParams.putString("ids", objectId);
1120
 
1121
            setRequest(new Request(session, "", pageIdRequestParams, HttpMethod.GET));
1122
        }
1123
 
1124
        @Override
1125
        protected void processSuccess(Response response) {
1126
            JSONObject results = Utility.tryGetJSONObjectFromResponse(response.getGraphObject(), objectId);
1127
            if (results != null) {
1128
                verifiedObjectId = results.optString("id");
1129
                objectIsPage = !Utility.isNullOrEmpty(verifiedObjectId);
1130
            }
1131
        }
1132
 
1133
        @Override
1134
        protected void processError(FacebookRequestError error) {
1135
            Logger.log(LoggingBehavior.REQUESTS,
1136
                    TAG,
1137
                    "Error getting the FB id for object '%s' : %s", objectId, error);
1138
        }
1139
    }
1140
 
1141
    private class PublishLikeRequestWrapper extends AbstractRequestWrapper {
1142
        String unlikeToken;
1143
 
1144
        PublishLikeRequestWrapper(String objectId) {
1145
            super(objectId);
1146
 
1147
            Bundle likeRequestParams = new Bundle();
1148
            likeRequestParams.putString("object", objectId);
1149
 
1150
            setRequest(new Request(session, "me/og.likes", likeRequestParams, HttpMethod.POST));
1151
        }
1152
 
1153
        @Override
1154
        protected void processSuccess(Response response) {
1155
            unlikeToken = Utility.safeGetStringFromResponse(response.getGraphObject(), "id");
1156
        }
1157
 
1158
        @Override
1159
        protected void processError(FacebookRequestError error) {
1160
            int errorCode = error.getErrorCode();
1161
            if (errorCode == ERROR_CODE_OBJECT_ALREADY_LIKED) {
1162
                // This isn't an error for us. Client was just out of sync with server
1163
                // This will prevent us from showing the dialog for this.
1164
 
1165
                // However, there is no unliketoken. So a subsequent unlike WILL show the dialog
1166
                this.error = null;
1167
            } else {
1168
                Logger.log(LoggingBehavior.REQUESTS,
1169
                        TAG,
1170
                        "Error liking object '%s' : %s", objectId, error);
1171
                logAppEventForError("publish_like", error);
1172
            }
1173
        }
1174
    }
1175
 
1176
    private class PublishUnlikeRequestWrapper extends AbstractRequestWrapper {
1177
        private String unlikeToken;
1178
 
1179
        PublishUnlikeRequestWrapper(String unlikeToken) {
1180
            super(null);
1181
 
1182
            this.unlikeToken = unlikeToken;
1183
 
1184
            setRequest(new Request(session, unlikeToken, null, HttpMethod.DELETE));
1185
        }
1186
 
1187
        @Override
1188
        protected void processSuccess(Response response) {
1189
        }
1190
 
1191
        @Override
1192
        protected void processError(FacebookRequestError error) {
1193
            Logger.log(LoggingBehavior.REQUESTS,
1194
                    TAG,
1195
                    "Error unliking object with unlike token '%s' : %s", unlikeToken, error);
1196
            logAppEventForError("publish_unlike", error);
1197
        }
1198
    }
1199
 
1200
    private class GetOGObjectLikesRequestWrapper extends AbstractRequestWrapper {
1201
        // Initialize the like status to what we currently have. This way, empty/error responses don't end
1202
        // up clearing out the state.
1203
        boolean objectIsLiked = LikeActionController.this.isObjectLiked;
1204
        String unlikeToken;
1205
 
1206
        GetOGObjectLikesRequestWrapper(String objectId) {
1207
            super(objectId);
1208
 
1209
            Bundle requestParams = new Bundle();
1210
            requestParams.putString("fields", "id,application");
1211
            requestParams.putString("object", objectId);
1212
 
1213
            setRequest(new Request(session, "me/og.likes", requestParams, HttpMethod.GET));
1214
        }
1215
 
1216
        @Override
1217
        protected void processSuccess(Response response) {
1218
            JSONArray dataSet = Utility.tryGetJSONArrayFromResponse(response.getGraphObject(), "data");
1219
            if (dataSet != null) {
1220
                for (int i = 0; i < dataSet.length(); i++) {
1221
                    JSONObject data = dataSet.optJSONObject(i);
1222
                    if (data != null) {
1223
                        objectIsLiked = true;
1224
                        JSONObject appData = data.optJSONObject("application");
1225
                        if (appData != null) {
1226
                            if (Utility.areObjectsEqual(session.getApplicationId(), appData.optString("id"))) {
1227
                                unlikeToken = data.optString("id");
1228
                            }
1229
                        }
1230
                    }
1231
                }
1232
            }
1233
        }
1234
 
1235
        @Override
1236
        protected void processError(FacebookRequestError error) {
1237
            Logger.log(LoggingBehavior.REQUESTS,
1238
                    TAG,
1239
                    "Error fetching like status for object '%s' : %s", objectId, error);
1240
            logAppEventForError("get_og_object_like", error);
1241
        }
1242
    }
1243
 
1244
    private class GetEngagementRequestWrapper extends AbstractRequestWrapper {
1245
        // Initialize the like status to what we currently have. This way, empty/error responses don't end
1246
        // up clearing out the state.
1247
        String likeCountStringWithLike = LikeActionController.this.likeCountStringWithLike;
1248
        String likeCountStringWithoutLike = LikeActionController.this.likeCountStringWithoutLike;
1249
        String socialSentenceStringWithLike = LikeActionController.this.socialSentenceWithLike;
1250
        String socialSentenceStringWithoutLike = LikeActionController.this.socialSentenceWithoutLike;
1251
 
1252
        GetEngagementRequestWrapper(String objectId) {
1253
            super(objectId);
1254
 
1255
            Bundle requestParams = new Bundle();
1256
            requestParams.putString(
1257
                    "fields",
1258
                    "engagement.fields(" +
1259
                            "count_string_with_like," +
1260
                            "count_string_without_like," +
1261
                            "social_sentence_with_like," +
1262
                            "social_sentence_without_like)");
1263
 
1264
            setRequest(new Request(session, objectId, requestParams, HttpMethod.GET));
1265
        }
1266
 
1267
        @Override
1268
        protected void processSuccess(Response response) {
1269
            JSONObject engagementResults = Utility.tryGetJSONObjectFromResponse(response.getGraphObject(), "engagement");
1270
            if (engagementResults != null) {
1271
                // Missing properties in the response should default to cached like status
1272
                likeCountStringWithLike =
1273
                        engagementResults.optString("count_string_with_like", likeCountStringWithLike);
1274
 
1275
                likeCountStringWithoutLike =
1276
                        engagementResults.optString("count_string_without_like", likeCountStringWithoutLike);
1277
 
1278
                socialSentenceStringWithLike =
1279
                        engagementResults.optString("social_sentence_with_like", socialSentenceStringWithLike);
1280
 
1281
                socialSentenceStringWithoutLike =
1282
                        engagementResults.optString("social_sentence_without_like", socialSentenceStringWithoutLike);
1283
            }
1284
        }
1285
 
1286
        @Override
1287
        protected void processError(FacebookRequestError error) {
1288
            Logger.log(LoggingBehavior.REQUESTS,
1289
                    TAG,
1290
                    "Error fetching engagement for object '%s' : %s", objectId, error);
1291
            logAppEventForError("get_engagement", error);
1292
        }
1293
    }
1294
 
1295
    private abstract class AbstractRequestWrapper {
1296
        private Request request;
1297
        protected String objectId;
1298
 
1299
        FacebookRequestError error;
1300
 
1301
        protected AbstractRequestWrapper(String objectId) {
1302
            this.objectId = objectId;
1303
        }
1304
 
1305
        void addToBatch(RequestBatch batch) {
1306
            batch.add(request);
1307
        }
1308
 
1309
        protected void setRequest(Request request) {
1310
            this.request = request;
1311
            // Make sure that our requests are hitting the latest version of the API known to this sdk.
1312
            request.setVersion(ServerProtocol.GRAPH_API_VERSION);
1313
            request.setCallback(new Request.Callback() {
1314
                @Override
1315
                public void onCompleted(Response response) {
1316
                    error = response.getError();
1317
                    if (error != null) {
1318
                        processError(error);
1319
                    } else {
1320
                        processSuccess(response);
1321
                    }
1322
                }
1323
            });
1324
        }
1325
 
1326
        protected void processError(FacebookRequestError error) {
1327
            Logger.log(LoggingBehavior.REQUESTS,
1328
                    TAG,
1329
                    "Error running request for object '%s' : %s", objectId, error);
1330
        }
1331
 
1332
        protected abstract void processSuccess(Response response);
1333
    }
1334
 
1335
    private enum LikeDialogFeature implements FacebookDialog.DialogFeature {
1336
 
1337
        LIKE_DIALOG(NativeProtocol.PROTOCOL_VERSION_20140701);
1338
 
1339
        private int minVersion;
1340
 
1341
        private LikeDialogFeature(int minVersion) {
1342
            this.minVersion = minVersion;
1343
        }
1344
 
1345
        public String getAction() {
1346
            return NativeProtocol.ACTION_LIKE_DIALOG;
1347
        }
1348
 
1349
        public int getMinVersion() {
1350
            return minVersion;
1351
        }
1352
    }
1353
 
1354
    private static class LikeDialogBuilder extends FacebookDialog.Builder<LikeDialogBuilder> {
1355
        private String objectId;
1356
 
1357
        public LikeDialogBuilder(Activity activity, String objectId) {
1358
            super(activity);
1359
 
1360
            this.objectId = objectId;
1361
        }
1362
 
1363
        @Override
1364
        protected EnumSet<? extends FacebookDialog.DialogFeature> getDialogFeatures() {
1365
            return EnumSet.of(LikeDialogFeature.LIKE_DIALOG);
1366
        }
1367
 
1368
        @Override
1369
        protected Bundle getMethodArguments() {
1370
            Bundle methodArgs = new Bundle();
1371
 
1372
            methodArgs.putString(NativeProtocol.METHOD_ARGS_OBJECT_ID, objectId);
1373
 
1374
            return methodArgs;
1375
        }
1376
 
1377
        public FacebookDialog.PendingCall getAppCall() {
1378
            return appCall;
1379
        }
1380
 
1381
        public String getApplicationId() {
1382
            return applicationId;
1383
        }
1384
 
1385
        public String getWebFallbackUrl() {
1386
            return getWebFallbackUrlInternal();
1387
        }
1388
    }
1389
 
1390
    // Performs cache re-ordering/trimming to keep most-recently-used items up front
1391
    // ** NOTE ** It is expected that only _ONE_ MRUCacheWorkItem is ever running. This is enforced by
1392
    // setting the concurrency of the WorkQueue to 1. Changing the concurrency will most likely lead to errors.
1393
    private static class MRUCacheWorkItem implements Runnable {
1394
        private static ArrayList<String> mruCachedItems = new ArrayList<String>();
1395
        private String cacheItem;
1396
        private boolean shouldTrim;
1397
 
1398
        MRUCacheWorkItem(String cacheItem, boolean shouldTrim) {
1399
            this.cacheItem = cacheItem;
1400
            this.shouldTrim = shouldTrim;
1401
        }
1402
 
1403
        @Override
1404
        public void run() {
1405
            if (cacheItem != null) {
1406
                mruCachedItems.remove(cacheItem);
1407
                mruCachedItems.add(0, cacheItem);
1408
            }
1409
            if (shouldTrim && mruCachedItems.size() >= MAX_CACHE_SIZE) {
1410
                int targetSize = MAX_CACHE_SIZE / 2; // Optimize for fewer trim-passes.
1411
                while (targetSize < mruCachedItems.size()) {
1412
                    String cacheKey = mruCachedItems.remove(mruCachedItems.size() - 1);
1413
 
1414
                    // Here is where we actually remove from the cache of LikeActionControllers.
1415
                    cache.remove(cacheKey);
1416
                }
1417
            }
1418
        }
1419
    }
1420
 
1421
    private static class SerializeToDiskWorkItem implements Runnable {
1422
        private String cacheKey;
1423
        private String controllerJson;
1424
 
1425
        SerializeToDiskWorkItem(String cacheKey, String controllerJson) {
1426
            this.cacheKey = cacheKey;
1427
            this.controllerJson = controllerJson;
1428
        }
1429
 
1430
        @Override
1431
        public void run() {
1432
            serializeToDiskSynchronously(cacheKey, controllerJson);
1433
        }
1434
    }
1435
 
1436
    private static class CreateLikeActionControllerWorkItem implements Runnable {
1437
        private Context context;
1438
        private String objectId;
1439
        private CreationCallback callback;
1440
 
1441
        CreateLikeActionControllerWorkItem(Context context, String objectId, CreationCallback callback) {
1442
            this.context = context;
1443
            this.objectId = objectId;
1444
            this.callback = callback;
1445
        }
1446
 
1447
        @Override
1448
        public void run() {
1449
            createControllerForObjectId(context, objectId, callback);
1450
        }
1451
    }
1452
}