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;
18
 
19
import android.app.Activity;
20
import android.content.ComponentName;
21
import android.content.Context;
22
import android.content.Intent;
23
import android.os.Bundle;
24
import android.support.v4.content.LocalBroadcastManager;
25
import android.util.Log;
26
import bolts.AppLinks;
27
import com.facebook.internal.AttributionIdentifiers;
28
import com.facebook.internal.Logger;
29
import com.facebook.internal.Utility;
30
import com.facebook.internal.Validate;
31
import com.facebook.model.GraphObject;
32
import org.json.JSONArray;
33
import org.json.JSONException;
34
import org.json.JSONObject;
35
 
36
import java.io.BufferedInputStream;
37
import java.io.BufferedOutputStream;
38
import java.io.FileNotFoundException;
39
import java.io.ObjectInputStream;
40
import java.io.ObjectOutputStream;
41
import java.io.Serializable;
42
import java.io.UnsupportedEncodingException;
43
import java.math.BigDecimal;
44
import java.util.ArrayList;
45
import java.util.Currency;
46
import java.util.HashMap;
47
import java.util.HashSet;
48
import java.util.List;
49
import java.util.Map;
50
import java.util.Set;
51
import java.util.concurrent.ConcurrentHashMap;
52
import java.util.concurrent.ScheduledThreadPoolExecutor;
53
import java.util.concurrent.TimeUnit;
54
 
55
 
56
/**
57
 * <p>
58
 * The AppEventsLogger class allows the developer to log various types of events back to Facebook.  In order to log
59
 * events, the app must create an instance of this class via a {@link #newLogger newLogger} method, and then call
60
 * the various "log" methods off of that.
61
 * </p>
62
 * <p>
63
 * This client-side event logging is then available through Facebook App Insights
64
 * and for use with Facebook Ads conversion tracking and optimization.
65
 * </p>
66
 * <p>
67
 * The AppEventsLogger class has a few related roles:
68
 * </p>
69
 * <ul>
70
 * <li>
71
 * Logging predefined and application-defined events to Facebook App Insights with a
72
 * numeric value to sum across a large number of events, and an optional set of key/value
73
 * parameters that define "segments" for this event (e.g., 'purchaserStatus' : 'frequent', or
74
 * 'gamerLevel' : 'intermediate').  These events may also be used for ads conversion tracking,
75
 * optimization, and other ads related targeting in the future.
76
 * </li>
77
 * <li>
78
 * Methods that control the way in which events are flushed out to the Facebook servers.
79
 * </li>
80
 * </ul>
81
 * Here are some important characteristics of the logging mechanism provided by AppEventsLogger:
82
 * <ul>
83
 * <li>
84
 * Events are not sent immediately when logged.  They're cached and flushed out to the Facebook servers
85
 * in a number of situations:
86
 * <ul>
87
 * <li>when an event count threshold is passed (currently 100 logged events).</li>
88
 * <li>when a time threshold is passed (currently 15 seconds).</li>
89
 * <li>when an app has gone to background and is then brought back to the foreground.</li>
90
 * </ul>
91
 * <li>
92
 * Events will be accumulated when the app is in a disconnected state, and sent when the connection is
93
 * restored and one of the above 'flush' conditions are met.
94
 * </li>
95
 * <li>
96
 * The AppEventsLogger class is intended to be used from the thread it was created on.  Multiple AppEventsLoggers
97
 * may be created on other threads if desired.
98
 * </li>
99
 * <li>
100
 * The developer can call the setFlushBehavior method to force the flushing of events to only
101
 * occur on an explicit call to the `flush` method.
102
 * </li>
103
 * <li>
104
 * The developer can turn on console debug output for event logging and flushing to the server
105
 * Settings.addLoggingBehavior(LoggingBehavior.APP_EVENTS);
106
 * </li>
107
 * </ul>
108
 * Some things to note when logging events:
109
 * <ul>
110
 * <li>
111
 * There is a limit on the number of unique event names an app can use, on the order of 300.
112
 * </li>
113
 * <li>
114
 * There is a limit to the number of unique parameter names in the provided parameters that can
115
 * be used per event, on the order of 25.  This is not just for an individual call, but for all
116
 * invocations for that eventName.
117
 * </li>
118
 * <li>
119
 * Event names and parameter names (the keys in the NSDictionary) must be between 2 and 40 characters, and
120
 * must consist of alphanumeric characters, _, -, or spaces.
121
 * </li>
122
 * <li>
123
 * The length of each parameter value can be no more than on the order of 100 characters.
124
 * </li>
125
 * </ul>
126
 */
127
public class AppEventsLogger {
128
    // Enums
129
 
130
    /**
131
     * Controls when an AppEventsLogger sends log events to the server
132
     */
133
    public enum FlushBehavior {
134
        /**
135
         * Flush automatically: periodically (once a minute or after every 100 events), and always at app reactivation.
136
         * This is the default value.
137
         */
138
        AUTO,
139
 
140
        /**
141
         * Only flush when AppEventsLogger.flush() is explicitly invoked.
142
         */
143
        EXPLICIT_ONLY,
144
    }
145
 
146
    // Constants
147
    private static final String TAG = AppEventsLogger.class.getCanonicalName();
148
 
149
    private static final int NUM_LOG_EVENTS_TO_TRY_TO_FLUSH_AFTER = 100;
150
    private static final int FLUSH_PERIOD_IN_SECONDS = 15;
151
    private static final int APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS = 60 * 60 * 24;
152
    private static final int FLUSH_APP_SESSION_INFO_IN_SECONDS = 30;
153
 
154
    private static final String SOURCE_APPLICATION_HAS_BEEN_SET_BY_THIS_INTENT = "_fbSourceApplicationHasBeenSet";
155
 
156
    // Instance member variables
157
    private final Context context;
158
    private final AccessTokenAppIdPair accessTokenAppId;
159
 
160
    private static Map<AccessTokenAppIdPair, SessionEventsState> stateMap =
161
            new ConcurrentHashMap<AccessTokenAppIdPair, SessionEventsState>();
162
    private static ScheduledThreadPoolExecutor backgroundExecutor;
163
    private static FlushBehavior flushBehavior = FlushBehavior.AUTO;
164
    private static boolean requestInFlight;
165
    private static Context applicationContext;
166
    private static Object staticLock = new Object();
167
    private static String hashedDeviceAndAppId;
168
    private static String sourceApplication;
169
    private static boolean isOpenedByApplink;
170
 
171
    // Rather than retaining Sessions, we extract the information we need and track app events by
172
    // application ID and access token (which may be null for Session-less calls). This avoids needing to
173
    // worry about Session lifecycle and also allows us to coalesce app events from different Sessions
174
    // that have the same access token/app ID.
175
    private static class AccessTokenAppIdPair implements Serializable {
176
        private static final long serialVersionUID = 1L;
177
        private final String accessToken;
178
        private final String applicationId;
179
 
180
        AccessTokenAppIdPair(Session session) {
181
            this(session.getAccessToken(), session.getApplicationId());
182
        }
183
 
184
        AccessTokenAppIdPair(String accessToken, String applicationId) {
185
            this.accessToken = Utility.isNullOrEmpty(accessToken) ? null : accessToken;
186
            this.applicationId = applicationId;
187
        }
188
 
189
        String getAccessToken() {
190
            return accessToken;
191
        }
192
 
193
        String getApplicationId() {
194
            return applicationId;
195
        }
196
 
197
        @Override
198
        public int hashCode() {
199
            return (accessToken == null ? 0 : accessToken.hashCode()) ^
200
                    (applicationId == null ? 0 : applicationId.hashCode());
201
        }
202
 
203
        @Override
204
        public boolean equals(Object o) {
205
            if (!(o instanceof AccessTokenAppIdPair)) {
206
                return false;
207
            }
208
            AccessTokenAppIdPair p = (AccessTokenAppIdPair) o;
209
            return Utility.areObjectsEqual(p.accessToken, accessToken) &&
210
                    Utility.areObjectsEqual(p.applicationId, applicationId);
211
        }
212
 
213
        private static class SerializationProxyV1 implements Serializable {
214
            private static final long serialVersionUID = -2488473066578201069L;
215
            private final String accessToken;
216
            private final String appId;
217
 
218
            private SerializationProxyV1(String accessToken, String appId) {
219
                this.accessToken = accessToken;
220
                this.appId = appId;
221
            }
222
 
223
            private Object readResolve() {
224
                return new AccessTokenAppIdPair(accessToken, appId);
225
            }
226
        }
227
 
228
        private Object writeReplace() {
229
            return new SerializationProxyV1(accessToken, applicationId);
230
        }
231
    }
232
 
233
    /**
234
     * This method is deprecated.  Use {@link Settings#getLimitEventAndDataUsage(Context)} instead.
235
     */
236
    @Deprecated
237
    public static boolean getLimitEventUsage(Context context) {
238
        return Settings.getLimitEventAndDataUsage(context);
239
    }
240
 
241
    /**
242
     * This method is deprecated.  Use {@link Settings#setLimitEventAndDataUsage(Context, boolean)} instead.
243
     */
244
    @Deprecated
245
    public static void setLimitEventUsage(Context context, boolean limitEventUsage) {
246
        Settings.setLimitEventAndDataUsage(context, limitEventUsage);
247
    }
248
 
249
    /**
250
     * Notifies the events system that the app has launched & logs an activatedApp event.  Should be called whenever
251
     * your app becomes active, typically in the onResume() method of each long-running Activity of your app.
252
     *
253
     * Use this method if your application ID is stored in application metadata, otherwise see
254
     * {@link AppEventsLogger#activateApp(android.content.Context, String)}.
255
     *
256
     * @param context Used to access the applicationId and the attributionId for non-authenticated users.
257
     */
258
    public static void activateApp(Context context) {
259
        Settings.sdkInitialize(context);
260
        activateApp(context, Utility.getMetadataApplicationId(context));
261
    }
262
 
263
    /**
264
     * Notifies the events system that the app has launched & logs an activatedApp event.  Should be called whenever
265
     * your app becomes active, typically in the onResume() method of each long-running Activity of your app.
266
     *
267
     * @param context       Used to access the attributionId for non-authenticated users.
268
     * @param applicationId The specific applicationId to report the activation for.
269
     */
270
    @SuppressWarnings("deprecation")
271
    public static void activateApp(Context context, String applicationId) {
272
        if (context == null || applicationId == null) {
273
            throw new IllegalArgumentException("Both context and applicationId must be non-null");
274
        }
275
 
276
        if ((context instanceof Activity)) {
277
            setSourceApplication((Activity) context);
278
        } else {
279
          // If context is not an Activity, we cannot get intent nor calling activity.
280
          resetSourceApplication();
281
          Log.d(AppEventsLogger.class.getName(),
282
              "To set source application the context of activateApp must be an instance of Activity");
283
        }
284
 
285
        // activateApp supercedes publishInstall in the public API, so we need to explicitly invoke it, since the server
286
        // can't reliably infer install state for all conditions of an app activate.
287
        Settings.publishInstallAsync(context, applicationId, null);
288
 
289
        final AppEventsLogger logger = new AppEventsLogger(context, applicationId, null);
290
        final long eventTime = System.currentTimeMillis();
291
        final String sourceApplicationInfo = getSourceApplication();
292
        backgroundExecutor.execute(new Runnable() {
293
            @Override
294
            public void run() {
295
                logger.logAppSessionResumeEvent(eventTime, sourceApplicationInfo);
296
            }
297
        });
298
    }
299
 
300
    /**
301
     * Notifies the events system that the app has been deactivated (put in the background) and
302
     * tracks the application session information. Should be called whenever your app becomes
303
     * inactive, typically in the onPause() method of each long-running Activity of your app.
304
     *
305
     * Use this method if your application ID is stored in application metadata, otherwise see
306
     * {@link AppEventsLogger#deactivateApp(android.content.Context, String)}.
307
     *
308
     * @param context Used to access the applicationId and the attributionId for non-authenticated users.
309
     */
310
    public static void deactivateApp(Context context) {
311
        deactivateApp(context, Utility.getMetadataApplicationId(context));
312
    }
313
 
314
    /**
315
     * Notifies the events system that the app has been deactivated (put in the background) and
316
     * tracks the application session information. Should be called whenever your app becomes
317
     * inactive, typically in the onPause() method of each long-running Activity of your app.
318
     *
319
     * @param context       Used to access the attributionId for non-authenticated users.
320
     * @param applicationId The specific applicationId to track session information for.
321
     */
322
    public static void deactivateApp(Context context, String applicationId) {
323
        if (context == null || applicationId == null) {
324
            throw new IllegalArgumentException("Both context and applicationId must be non-null");
325
        }
326
 
327
        resetSourceApplication();
328
 
329
        final AppEventsLogger logger = new AppEventsLogger(context, applicationId, null);
330
        final long eventTime = System.currentTimeMillis();
331
        backgroundExecutor.execute(new Runnable() {
332
            @Override
333
            public void run() {
334
                logger.logAppSessionSuspendEvent(eventTime);
335
            }
336
        });
337
    }
338
 
339
    private void logAppSessionResumeEvent(long eventTime, String sourceApplicationInfo) {
340
        PersistedAppSessionInfo.onResume(applicationContext, accessTokenAppId, this, eventTime, sourceApplicationInfo);
341
    }
342
 
343
    private void logAppSessionSuspendEvent(long eventTime) {
344
        PersistedAppSessionInfo.onSuspend(applicationContext, accessTokenAppId, this, eventTime);
345
    }
346
 
347
    /**
348
     * Build an AppEventsLogger instance to log events through.  The Facebook app that these events are targeted at
349
     * comes from this application's metadata. The application ID used to log events will be determined from
350
     * the app ID specified in the package metadata.
351
     *
352
     * @param context Used to access the applicationId and the attributionId for non-authenticated users.
353
     * @return AppEventsLogger instance to invoke log* methods on.
354
     */
355
    public static AppEventsLogger newLogger(Context context) {
356
        return new AppEventsLogger(context, null, null);
357
    }
358
 
359
    /**
360
     * Build an AppEventsLogger instance to log events through.
361
     *
362
     * @param context Used to access the attributionId for non-authenticated users.
363
     * @param session Explicitly specified Session to log events against.  If null, the activeSession
364
     *                will be used if it's open, otherwise the logging will happen against the default
365
     *                app ID specified via the app ID specified in the package metadata.
366
     * @return AppEventsLogger instance to invoke log* methods on.
367
     */
368
    public static AppEventsLogger newLogger(Context context, Session session) {
369
        return new AppEventsLogger(context, null, session);
370
    }
371
 
372
    /**
373
     * Build an AppEventsLogger instance to log events through.
374
     *
375
     * @param context       Used to access the attributionId for non-authenticated users.
376
     * @param applicationId Explicitly specified Facebook applicationId to log events against.  If null, the default
377
     *                      app ID specified in the package metadata will be used.
378
     * @param session       Explicitly specified Session to log events against.  If null, the activeSession
379
     *                      will be used if it's open, otherwise the logging will happen against the specified
380
     *                      app ID.
381
     * @return AppEventsLogger instance to invoke log* methods on.
382
     */
383
    public static AppEventsLogger newLogger(Context context, String applicationId, Session session) {
384
        return new AppEventsLogger(context, applicationId, session);
385
    }
386
 
387
    /**
388
     * Build an AppEventsLogger instance to log events that are attributed to the application but not to
389
     * any particular Session.
390
     *
391
     * @param context       Used to access the attributionId for non-authenticated users.
392
     * @param applicationId Explicitly specified Facebook applicationId to log events against.  If null, the default
393
     *                      app ID specified
394
     *                      in the package metadata will be used.
395
     * @return AppEventsLogger instance to invoke log* methods on.
396
     */
397
    public static AppEventsLogger newLogger(Context context, String applicationId) {
398
        return new AppEventsLogger(context, applicationId, null);
399
    }
400
 
401
    /**
402
     * The action used to indicate that a flush of app events has occurred. This should
403
     * be used as an action in an IntentFilter and BroadcastReceiver registered with
404
     * the {@link android.support.v4.content.LocalBroadcastManager}.
405
     */
406
    public static final String ACTION_APP_EVENTS_FLUSHED = "com.facebook.sdk.APP_EVENTS_FLUSHED";
407
 
408
    public static final String APP_EVENTS_EXTRA_NUM_EVENTS_FLUSHED = "com.facebook.sdk.APP_EVENTS_NUM_EVENTS_FLUSHED";
409
    public static final String APP_EVENTS_EXTRA_FLUSH_RESULT = "com.facebook.sdk.APP_EVENTS_FLUSH_RESULT";
410
 
411
    /**
412
     * Access the behavior that AppEventsLogger uses to determine when to flush logged events to the server. This
413
     * setting applies to all instances of AppEventsLogger.
414
     *
415
     * @return specified flush behavior.
416
     */
417
    public static FlushBehavior getFlushBehavior() {
418
        synchronized (staticLock) {
419
            return flushBehavior;
420
        }
421
    }
422
 
423
    /**
424
     * Set the behavior that this AppEventsLogger uses to determine when to flush logged events to the server. This
425
     * setting applies to all instances of AppEventsLogger.
426
     *
427
     * @param flushBehavior the desired behavior.
428
     */
429
    public static void setFlushBehavior(FlushBehavior flushBehavior) {
430
        synchronized (staticLock) {
431
            AppEventsLogger.flushBehavior = flushBehavior;
432
        }
433
    }
434
 
435
    /**
436
     * Log an app event with the specified name.
437
     *
438
     * @param eventName eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
439
     *                  {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
440
     *                  constants are applicable.
441
     *                  Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
442
     *                  or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
443
     *                  have no more than ~300 distinct event names.
444
     */
445
    public void logEvent(String eventName) {
446
        logEvent(eventName, null);
447
    }
448
 
449
    /**
450
     * Log an app event with the specified name and the supplied value.
451
     *
452
     * @param eventName  eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
453
     *                   {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
454
     *                   constants are applicable.
455
     *                   Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
456
     *                   or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
457
     *                   have no more than ~300 distinct event names.
458
     *                   * @param eventName
459
     * @param valueToSum a value to associate with the event which will be summed up in Insights for across all
460
     *                   instances of the event, so that average values can be determined, etc.
461
     */
462
    public void logEvent(String eventName, double valueToSum) {
463
        logEvent(eventName, valueToSum, null);
464
    }
465
 
466
    /**
467
     * Log an app event with the specified name and set of parameters.
468
     *
469
     * @param eventName  eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
470
     *                   {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
471
     *                   constants are applicable.
472
     *                   Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
473
     *                   or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
474
     *                   have no more than ~300 distinct event names.
475
     * @param parameters A Bundle of parameters to log with the event.  Insights will allow looking at the logs of these
476
     *                   events via different parameter values.  You can log on the order of 10 parameters with each
477
     *                   distinct eventName.  It's advisable to keep the number of unique values provided for each
478
     *                   parameter in the, at most, thousands.  As an example, don't attempt to provide a unique
479
     *                   parameter value for each unique user in your app.  You won't get meaningful aggregate reporting
480
     *                   on so many parameter values.  The values in the bundles should be Strings or numeric values.
481
     */
482
    public void logEvent(String eventName, Bundle parameters) {
483
        logEvent(eventName, null, parameters, false);
484
    }
485
 
486
    /**
487
     * Log an app event with the specified name, supplied value, and set of parameters.
488
     *
489
     * @param eventName  eventName used to denote the event.  Choose amongst the EVENT_NAME_* constants in
490
     *                   {@link AppEventsConstants} when possible.  Or create your own if none of the EVENT_NAME_*
491
     *                   constants are applicable.
492
     *                   Event names should be 40 characters or less, alphanumeric, and can include spaces, underscores
493
     *                   or hyphens, but mustn't have a space or hyphen as the first character.  Any given app should
494
     *                   have no more than ~300 distinct event names.
495
     * @param valueToSum a value to associate with the event which will be summed up in Insights for across all
496
     *                   instances of the event, so that average values can be determined, etc.
497
     * @param parameters A Bundle of parameters to log with the event.  Insights will allow looking at the logs of these
498
     *                   events via different parameter values.  You can log on the order of 10 parameters with each
499
     *                   distinct eventName.  It's advisable to keep the number of unique values provided for each
500
     *                   parameter in the, at most, thousands.  As an example, don't attempt to provide a unique
501
     *                   parameter value for each unique user in your app.  You won't get meaningful aggregate reporting
502
     *                   on so many parameter values.  The values in the bundles should be Strings or numeric values.
503
     */
504
    public void logEvent(String eventName, double valueToSum, Bundle parameters) {
505
        logEvent(eventName, valueToSum, parameters, false);
506
    }
507
 
508
    /**
509
     * Logs a purchase event with Facebook, in the specified amount and with the specified currency.
510
     *
511
     * @param purchaseAmount Amount of purchase, in the currency specified by the 'currency' parameter. This value
512
     *                       will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
513
     * @param currency       Currency used to specify the amount.
514
     */
515
    public void logPurchase(BigDecimal purchaseAmount, Currency currency) {
516
        logPurchase(purchaseAmount, currency, null);
517
    }
518
 
519
    /**
520
     * Logs a purchase event with Facebook, in the specified amount and with the specified currency.  Additional
521
     * detail about the purchase can be passed in through the parameters bundle.
522
     *
523
     * @param purchaseAmount Amount of purchase, in the currency specified by the 'currency' parameter. This value
524
     *                       will be rounded to the thousandths place (e.g., 12.34567 becomes 12.346).
525
     * @param currency       Currency used to specify the amount.
526
     * @param parameters     Arbitrary additional information for describing this event.  Should have no more than
527
     *                       10 entries, and keys should be mostly consistent from one purchase event to the next.
528
     */
529
    public void logPurchase(BigDecimal purchaseAmount, Currency currency, Bundle parameters) {
530
 
531
        if (purchaseAmount == null) {
532
            notifyDeveloperError("purchaseAmount cannot be null");
533
            return;
534
        } else if (currency == null) {
535
            notifyDeveloperError("currency cannot be null");
536
            return;
537
        }
538
 
539
        if (parameters == null) {
540
            parameters = new Bundle();
541
        }
542
        parameters.putString(AppEventsConstants.EVENT_PARAM_CURRENCY, currency.getCurrencyCode());
543
 
544
        logEvent(AppEventsConstants.EVENT_NAME_PURCHASED, purchaseAmount.doubleValue(), parameters);
545
        eagerFlush();
546
    }
547
 
548
    /**
549
     * Explicitly flush any stored events to the server.  Implicit flushes may happen depending on the value
550
     * of getFlushBehavior.  This method allows for explicit, app invoked flushing.
551
     */
552
    public void flush() {
553
        flush(FlushReason.EXPLICIT);
554
    }
555
 
556
    /**
557
     * Call this when the consuming Activity/Fragment receives an onStop() callback in order to persist any
558
     * outstanding events to disk, so they may be flushed at a later time. The next flush (explicit or not)
559
     * will check for any outstanding events and, if present, include them in that flush. Note that this call
560
     * may trigger an I/O operation on the calling thread. Explicit use of this method is not necessary
561
     * if the consumer is making use of {@link UiLifecycleHelper}, which will take care of making the call
562
     * in its own onStop() callback.
563
     */
564
    public static void onContextStop() {
565
        PersistedEvents.persistEvents(applicationContext, stateMap);
566
    }
567
 
568
    boolean isValidForSession(Session session) {
569
        AccessTokenAppIdPair other = new AccessTokenAppIdPair(session);
570
        return accessTokenAppId.equals(other);
571
    }
572
 
573
    /**
574
     * This method is intended only for internal use by the Facebook SDK and other use is unsupported.
575
     */
576
    public void logSdkEvent(String eventName, Double valueToSum, Bundle parameters) {
577
        logEvent(eventName, valueToSum, parameters, true);
578
    }
579
 
580
    /**
581
     * Returns the app ID this logger was configured to log to.
582
     *
583
     * @return the Facebook app ID
584
     */
585
    public String getApplicationId() {
586
        return accessTokenAppId.getApplicationId();
587
    }
588
 
589
    //
590
    // Private implementation
591
    //
592
 
593
    @SuppressWarnings("UnusedDeclaration")
594
    private enum FlushReason {
595
        EXPLICIT,
596
        TIMER,
597
        SESSION_CHANGE,
598
        PERSISTED_EVENTS,
599
        EVENT_THRESHOLD,
600
        EAGER_FLUSHING_EVENT,
601
    }
602
 
603
    @SuppressWarnings("UnusedDeclaration")
604
    private enum FlushResult {
605
        SUCCESS,
606
        SERVER_ERROR,
607
        NO_CONNECTIVITY,
608
        UNKNOWN_ERROR
609
    }
610
 
611
    /**
612
     * Constructor is private, newLogger() methods should be used to build an instance.
613
     */
614
    private AppEventsLogger(Context context, String applicationId, Session session) {
615
 
616
        Validate.notNull(context, "context");
617
        this.context = context;
618
 
619
        if (session == null) {
620
            session = Session.getActiveSession();
621
        }
622
 
623
        // If we have a session and the appId passed is null or matches the session's app ID:
624
        if (session != null &&
625
                (applicationId == null || applicationId.equals(session.getApplicationId()))
626
                ) {
627
            accessTokenAppId = new AccessTokenAppIdPair(session);
628
        } else {
629
            // If no app ID passed, get it from the manifest:
630
            if (applicationId == null) {
631
                applicationId = Utility.getMetadataApplicationId(context);
632
            }
633
            accessTokenAppId = new AccessTokenAppIdPair(null, applicationId);
634
        }
635
 
636
        synchronized (staticLock) {
637
 
638
            if (hashedDeviceAndAppId == null) {
639
                hashedDeviceAndAppId = Utility.getHashedDeviceAndAppID(context, applicationId);
640
            }
641
 
642
            if (applicationContext == null) {
643
                applicationContext = context.getApplicationContext();
644
            }
645
        }
646
 
647
        initializeTimersIfNeeded();
648
    }
649
 
650
    private static void initializeTimersIfNeeded() {
651
        synchronized (staticLock) {
652
            if (backgroundExecutor != null) {
653
                return;
654
            }
655
            backgroundExecutor = new ScheduledThreadPoolExecutor(1);
656
        }
657
 
658
        final Runnable flushRunnable = new Runnable() {
659
            @Override
660
            public void run() {
661
                if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
662
                    flushAndWait(FlushReason.TIMER);
663
                }
664
            }
665
        };
666
 
667
        backgroundExecutor.scheduleAtFixedRate(
668
                flushRunnable,
669
                0,
670
                FLUSH_PERIOD_IN_SECONDS,
671
                TimeUnit.SECONDS
672
        );
673
 
674
        final Runnable attributionRecheckRunnable = new Runnable() {
675
            @Override
676
            public void run() {
677
                Set<String> applicationIds = new HashSet<String>();
678
                synchronized (staticLock) {
679
                    for (AccessTokenAppIdPair accessTokenAppId : stateMap.keySet()) {
680
                        applicationIds.add(accessTokenAppId.getApplicationId());
681
                    }
682
                }
683
                for (String applicationId : applicationIds) {
684
                    Utility.queryAppSettings(applicationId, true);
685
                }
686
            }
687
        };
688
 
689
        backgroundExecutor.scheduleAtFixedRate(
690
                attributionRecheckRunnable,
691
                0,
692
                APP_SUPPORTS_ATTRIBUTION_ID_RECHECK_PERIOD_IN_SECONDS,
693
                TimeUnit.SECONDS
694
        );
695
    }
696
 
697
    private void logEvent(String eventName, Double valueToSum, Bundle parameters, boolean isImplicitlyLogged) {
698
        AppEvent event = new AppEvent(this.context, eventName, valueToSum, parameters, isImplicitlyLogged);
699
        logEvent(context, event, accessTokenAppId);
700
    }
701
 
702
    private static void logEvent(final Context context,
703
                                 final AppEvent event,
704
                                 final AccessTokenAppIdPair accessTokenAppId) {
705
        Settings.getExecutor().execute(new Runnable() {
706
            @Override
707
            public void run() {
708
                SessionEventsState state = getSessionEventsState(context, accessTokenAppId);
709
                state.addEvent(event);
710
                flushIfNecessary();
711
            }
712
        });
713
    }
714
 
715
    static void eagerFlush() {
716
        if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
717
            flush(FlushReason.EAGER_FLUSHING_EVENT);
718
        }
719
    }
720
 
721
    private static void flushIfNecessary() {
722
        synchronized (staticLock) {
723
            if (getFlushBehavior() != FlushBehavior.EXPLICIT_ONLY) {
724
                if (getAccumulatedEventCount() > NUM_LOG_EVENTS_TO_TRY_TO_FLUSH_AFTER) {
725
                    flush(FlushReason.EVENT_THRESHOLD);
726
                }
727
            }
728
        }
729
    }
730
 
731
    private static int getAccumulatedEventCount() {
732
        synchronized (staticLock) {
733
 
734
            int result = 0;
735
            for (SessionEventsState state : stateMap.values()) {
736
                result += state.getAccumulatedEventCount();
737
            }
738
            return result;
739
        }
740
    }
741
 
742
    // Creates a new SessionEventsState if not already in the map.
743
    private static SessionEventsState getSessionEventsState(Context context, AccessTokenAppIdPair accessTokenAppId) {
744
        // Do this work outside of the lock to prevent deadlocks in implementation of
745
        //  AdvertisingIdClient.getAdvertisingIdInfo, because that implementation blocks waiting on the main thread,
746
        //  which may also grab this staticLock.
747
        SessionEventsState state = stateMap.get(accessTokenAppId);
748
        AttributionIdentifiers attributionIdentifiers = null;
749
        if (state == null) {
750
            // Retrieve attributionId, but we will only send it if attribution is supported for the app.
751
            attributionIdentifiers = AttributionIdentifiers.getAttributionIdentifiers(context);
752
        }
753
 
754
        synchronized (staticLock) {
755
            // Check state again while we're locked.
756
            state = stateMap.get(accessTokenAppId);
757
            if (state == null) {
758
                state = new SessionEventsState(attributionIdentifiers, context.getPackageName(), hashedDeviceAndAppId);
759
                stateMap.put(accessTokenAppId, state);
760
            }
761
            return state;
762
        }
763
    }
764
 
765
    private static SessionEventsState getSessionEventsState(AccessTokenAppIdPair accessTokenAppId) {
766
        synchronized (staticLock) {
767
            return stateMap.get(accessTokenAppId);
768
        }
769
    }
770
 
771
    private static void flush(final FlushReason reason) {
772
 
773
        Settings.getExecutor().execute(new Runnable() {
774
            @Override
775
            public void run() {
776
                flushAndWait(reason);
777
            }
778
        });
779
    }
780
 
781
    private static void flushAndWait(final FlushReason reason) {
782
 
783
        Set<AccessTokenAppIdPair> keysToFlush;
784
        synchronized (staticLock) {
785
            if (requestInFlight) {
786
                return;
787
            }
788
            requestInFlight = true;
789
            keysToFlush = new HashSet<AccessTokenAppIdPair>(stateMap.keySet());
790
        }
791
 
792
        accumulatePersistedEvents();
793
 
794
        FlushStatistics flushResults = null;
795
        try {
796
            flushResults = buildAndExecuteRequests(reason, keysToFlush);
797
        } catch (Exception e) {
798
            Utility.logd(TAG, "Caught unexpected exception while flushing: ", e);
799
        }
800
 
801
        synchronized (staticLock) {
802
            requestInFlight = false;
803
        }
804
 
805
        if (flushResults != null) {
806
            final Intent intent = new Intent(ACTION_APP_EVENTS_FLUSHED);
807
            intent.putExtra(APP_EVENTS_EXTRA_NUM_EVENTS_FLUSHED, flushResults.numEvents);
808
            intent.putExtra(APP_EVENTS_EXTRA_FLUSH_RESULT, flushResults.result);
809
            LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent);
810
        }
811
    }
812
 
813
    private static FlushStatistics buildAndExecuteRequests(FlushReason reason, Set<AccessTokenAppIdPair> keysToFlush) {
814
        FlushStatistics flushResults = new FlushStatistics();
815
 
816
        boolean limitEventUsage = Settings.getLimitEventAndDataUsage(applicationContext);
817
 
818
        List<Request> requestsToExecute = new ArrayList<Request>();
819
        for (AccessTokenAppIdPair accessTokenAppId : keysToFlush) {
820
            SessionEventsState sessionEventsState = getSessionEventsState(accessTokenAppId);
821
            if (sessionEventsState == null) {
822
                continue;
823
            }
824
 
825
            Request request = buildRequestForSession(accessTokenAppId, sessionEventsState, limitEventUsage,
826
                    flushResults);
827
            if (request != null) {
828
                requestsToExecute.add(request);
829
            }
830
        }
831
 
832
        if (requestsToExecute.size() > 0) {
833
            Logger.log(LoggingBehavior.APP_EVENTS, TAG, "Flushing %d events due to %s.",
834
                    flushResults.numEvents,
835
                    reason.toString());
836
 
837
            for (Request request : requestsToExecute) {
838
                // Execute the request synchronously. Callbacks will take care of handling errors and updating
839
                // our final overall result.
840
                request.executeAndWait();
841
            }
842
            return flushResults;
843
        }
844
 
845
        return null;
846
    }
847
 
848
    private static class FlushStatistics {
849
        public int numEvents = 0;
850
        public FlushResult result = FlushResult.SUCCESS;
851
    }
852
 
853
    private static Request buildRequestForSession(final AccessTokenAppIdPair accessTokenAppId,
854
                                                  final SessionEventsState sessionEventsState,
855
                                                  final boolean limitEventUsage,
856
                                                  final FlushStatistics flushState) {
857
        String applicationId = accessTokenAppId.getApplicationId();
858
 
859
        Utility.FetchedAppSettings fetchedAppSettings = Utility.queryAppSettings(applicationId, false);
860
 
861
        final Request postRequest = Request.newPostRequest(
862
                null,
863
                String.format("%s/activities", applicationId),
864
                null,
865
                null);
866
 
867
        Bundle requestParameters = postRequest.getParameters();
868
        if (requestParameters == null) {
869
            requestParameters = new Bundle();
870
        }
871
        requestParameters.putString("access_token", accessTokenAppId.getAccessToken());
872
        postRequest.setParameters(requestParameters);
873
 
874
        if (fetchedAppSettings == null) {
875
            return null;
876
        }
877
 
878
        int numEvents = sessionEventsState.populateRequest(postRequest, fetchedAppSettings.supportsImplicitLogging(),
879
                fetchedAppSettings.supportsAttribution(), limitEventUsage);
880
        if (numEvents == 0) {
881
            return null;
882
        }
883
 
884
        flushState.numEvents += numEvents;
885
 
886
        postRequest.setCallback(new Request.Callback() {
887
            @Override
888
            public void onCompleted(Response response) {
889
                handleResponse(accessTokenAppId, postRequest, response, sessionEventsState, flushState);
890
            }
891
        });
892
 
893
        return postRequest;
894
    }
895
 
896
    private static void handleResponse(AccessTokenAppIdPair accessTokenAppId, Request request, Response response,
897
                                       SessionEventsState sessionEventsState, FlushStatistics flushState) {
898
        FacebookRequestError error = response.getError();
899
        String resultDescription = "Success";
900
 
901
        FlushResult flushResult = FlushResult.SUCCESS;
902
 
903
        if (error != null) {
904
            final int NO_CONNECTIVITY_ERROR_CODE = -1;
905
            if (error.getErrorCode() == NO_CONNECTIVITY_ERROR_CODE) {
906
                resultDescription = "Failed: No Connectivity";
907
                flushResult = FlushResult.NO_CONNECTIVITY;
908
            } else {
909
                resultDescription = String.format("Failed:\n  Response: %s\n  Error %s",
910
                        response.toString(),
911
                        error.toString());
912
                flushResult = FlushResult.SERVER_ERROR;
913
            }
914
        }
915
 
916
        if (Settings.isLoggingBehaviorEnabled(LoggingBehavior.APP_EVENTS)) {
917
            String eventsJsonString = (String) request.getTag();
918
            String prettyPrintedEvents;
919
 
920
            try {
921
                JSONArray jsonArray = new JSONArray(eventsJsonString);
922
                prettyPrintedEvents = jsonArray.toString(2);
923
            } catch (JSONException exc) {
924
                prettyPrintedEvents = "<Can't encode events for debug logging>";
925
            }
926
 
927
            Logger.log(LoggingBehavior.APP_EVENTS, TAG,
928
                    "Flush completed\nParams: %s\n  Result: %s\n  Events JSON: %s",
929
                    request.getGraphObject().toString(),
930
                    resultDescription,
931
                    prettyPrintedEvents);
932
        }
933
 
934
        sessionEventsState.clearInFlightAndStats(error != null);
935
 
936
        if (flushResult == FlushResult.NO_CONNECTIVITY) {
937
            // We may call this for multiple requests in a batch, which is slightly inefficient since in principle
938
            // we could call it once for all failed requests, but the impact is likely to be minimal.
939
            // We don't call this for other server errors, because if an event failed because it was malformed, etc.,
940
            // continually retrying it will cause subsequent events to not be logged either.
941
            PersistedEvents.persistEvents(applicationContext, accessTokenAppId, sessionEventsState);
942
        }
943
 
944
        if (flushResult != FlushResult.SUCCESS) {
945
            // We assume that connectivity issues are more significant to report than server issues.
946
            if (flushState.result != FlushResult.NO_CONNECTIVITY) {
947
                flushState.result = flushResult;
948
            }
949
        }
950
    }
951
 
952
    private static int accumulatePersistedEvents() {
953
        PersistedEvents persistedEvents = PersistedEvents.readAndClearStore(applicationContext);
954
 
955
        int result = 0;
956
        for (AccessTokenAppIdPair accessTokenAppId : persistedEvents.keySet()) {
957
            SessionEventsState sessionEventsState = getSessionEventsState(applicationContext, accessTokenAppId);
958
 
959
            List<AppEvent> events = persistedEvents.getEvents(accessTokenAppId);
960
            sessionEventsState.accumulatePersistedEvents(events);
961
            result += events.size();
962
        }
963
 
964
        return result;
965
    }
966
 
967
    /**
968
     * Invoke this method, rather than throwing an Exception, for situations where user/server input might reasonably
969
     * cause this to occur, and thus don't want an exception thrown at production time, but do want logging
970
     * notification.
971
     */
972
    private static void notifyDeveloperError(String message) {
973
        Logger.log(LoggingBehavior.DEVELOPER_ERRORS, "AppEvents", message);
974
    }
975
 
976
    /**
977
     * Source Application setters and getters
978
     */
979
    private static void setSourceApplication(Activity activity) {
980
 
981
        ComponentName callingApplication = activity.getCallingActivity();
982
        if (callingApplication != null) {
983
            String callingApplicationPackage = callingApplication.getPackageName();
984
            if (callingApplicationPackage.equals(activity.getPackageName())) {
985
                // open by own app.
986
                resetSourceApplication();
987
                return;
988
            }
989
            sourceApplication = callingApplicationPackage;
990
        }
991
 
992
        // Tap icon to open an app will still get the old intent if the activity was opened by an intent before.
993
        // Introduce an extra field in the intent to force clear the sourceApplication.
994
        Intent openIntent = activity.getIntent();
995
        if (openIntent == null || openIntent.getBooleanExtra(SOURCE_APPLICATION_HAS_BEEN_SET_BY_THIS_INTENT, false)) {
996
            resetSourceApplication();
997
            return;
998
        }
999
 
1000
        Bundle applinkData = AppLinks.getAppLinkData(openIntent);
1001
 
1002
        if (applinkData == null) {
1003
            resetSourceApplication();
1004
            return;
1005
        }
1006
 
1007
        isOpenedByApplink = true;
1008
 
1009
        Bundle applinkReferrerData = applinkData.getBundle("referer_app_link");
1010
 
1011
        if (applinkReferrerData == null) {
1012
            sourceApplication = null;
1013
            return;
1014
        }
1015
 
1016
        String applinkReferrerPackage = applinkReferrerData.getString("package");
1017
        sourceApplication = applinkReferrerPackage;
1018
 
1019
        // Mark this intent has been used to avoid use this intent again and again.
1020
        openIntent.putExtra(SOURCE_APPLICATION_HAS_BEEN_SET_BY_THIS_INTENT, true);
1021
 
1022
        return;
1023
    }
1024
 
1025
    static void setSourceApplication(String applicationPackage, boolean openByAppLink) {
1026
        sourceApplication = applicationPackage;
1027
        isOpenedByApplink = openByAppLink;
1028
    }
1029
 
1030
    static String getSourceApplication() {
1031
        String openType = "Unclassified";
1032
        if (isOpenedByApplink) {
1033
            openType = "Applink";
1034
        }
1035
        if (sourceApplication != null) {
1036
            return openType + "(" + sourceApplication + ")";
1037
        }
1038
        return openType;
1039
    }
1040
 
1041
    static void resetSourceApplication() {
1042
        sourceApplication = null;
1043
        isOpenedByApplink = false;
1044
    }
1045
 
1046
    //
1047
    // Deprecated Stuff
1048
    //
1049
 
1050
 
1051
    static class SessionEventsState {
1052
        private List<AppEvent> accumulatedEvents = new ArrayList<AppEvent>();
1053
        private List<AppEvent> inFlightEvents = new ArrayList<AppEvent>();
1054
        private int numSkippedEventsDueToFullBuffer;
1055
        private AttributionIdentifiers attributionIdentifiers;
1056
        private String packageName;
1057
        private String hashedDeviceAndAppId;
1058
 
1059
        public static final String EVENT_COUNT_KEY = "event_count";
1060
        public static final String ENCODED_EVENTS_KEY = "encoded_events";
1061
        public static final String NUM_SKIPPED_KEY = "num_skipped";
1062
 
1063
        private final int MAX_ACCUMULATED_LOG_EVENTS = 1000;
1064
 
1065
        public SessionEventsState(AttributionIdentifiers identifiers, String packageName, String hashedDeviceAndAppId) {
1066
            this.attributionIdentifiers = identifiers;
1067
            this.packageName = packageName;
1068
            this.hashedDeviceAndAppId = hashedDeviceAndAppId;
1069
        }
1070
 
1071
        // Synchronize here and in other methods on this class, because could be coming in from different
1072
        // AppEventsLoggers on different threads pointing at the same session.
1073
        public synchronized void addEvent(AppEvent event) {
1074
            if (accumulatedEvents.size() + inFlightEvents.size() >= MAX_ACCUMULATED_LOG_EVENTS) {
1075
                numSkippedEventsDueToFullBuffer++;
1076
            } else {
1077
                accumulatedEvents.add(event);
1078
            }
1079
        }
1080
 
1081
        public synchronized int getAccumulatedEventCount() {
1082
            return accumulatedEvents.size();
1083
        }
1084
 
1085
        public synchronized void clearInFlightAndStats(boolean moveToAccumulated) {
1086
            if (moveToAccumulated) {
1087
                accumulatedEvents.addAll(inFlightEvents);
1088
            }
1089
            inFlightEvents.clear();
1090
            numSkippedEventsDueToFullBuffer = 0;
1091
        }
1092
 
1093
        public int populateRequest(Request request, boolean includeImplicitEvents,
1094
                                   boolean includeAttribution, boolean limitEventUsage) {
1095
 
1096
            int numSkipped;
1097
            JSONArray jsonArray;
1098
            synchronized (this) {
1099
                numSkipped = numSkippedEventsDueToFullBuffer;
1100
 
1101
                // move all accumulated events to inFlight.
1102
                inFlightEvents.addAll(accumulatedEvents);
1103
                accumulatedEvents.clear();
1104
 
1105
                jsonArray = new JSONArray();
1106
                for (AppEvent event : inFlightEvents) {
1107
                    if (includeImplicitEvents || !event.getIsImplicit()) {
1108
                        jsonArray.put(event.getJSONObject());
1109
                    }
1110
                }
1111
 
1112
                if (jsonArray.length() == 0) {
1113
                    return 0;
1114
                }
1115
            }
1116
 
1117
            populateRequest(request, numSkipped, jsonArray, includeAttribution, limitEventUsage);
1118
            return jsonArray.length();
1119
        }
1120
 
1121
        public synchronized List<AppEvent> getEventsToPersist() {
1122
            // We will only persist accumulated events, not ones currently in-flight. This means if an in-flight
1123
            // request fails, those requests will not be persisted and thus might be lost if the process terminates
1124
            // while the flush is in progress.
1125
            List<AppEvent> result = accumulatedEvents;
1126
            accumulatedEvents = new ArrayList<AppEvent>();
1127
            return result;
1128
        }
1129
 
1130
        public synchronized void accumulatePersistedEvents(List<AppEvent> events) {
1131
            // We won't skip events due to a full buffer, since we already accumulated them once and persisted
1132
            // them. But they will count against the buffer size when further events are accumulated.
1133
            accumulatedEvents.addAll(events);
1134
        }
1135
 
1136
        private void populateRequest(Request request, int numSkipped, JSONArray events, boolean includeAttribution,
1137
                                     boolean limitEventUsage) {
1138
            GraphObject publishParams = GraphObject.Factory.create();
1139
            publishParams.setProperty("event", "CUSTOM_APP_EVENTS");
1140
 
1141
            if (numSkippedEventsDueToFullBuffer > 0) {
1142
                publishParams.setProperty("num_skipped_events", numSkipped);
1143
            }
1144
 
1145
            if (includeAttribution) {
1146
                Utility.setAppEventAttributionParameters(publishParams, attributionIdentifiers,
1147
                        hashedDeviceAndAppId, limitEventUsage);
1148
            }
1149
 
1150
            // The code to get all the Extended info is safe but just in case we can wrap the whole
1151
            // call in its own try/catch block since some of the things it does might cause
1152
            // unexpected exceptions on rooted/funky devices:
1153
            try {
1154
                Utility.setAppEventExtendedDeviceInfoParameters(publishParams, applicationContext);
1155
            } catch (Exception e) {
1156
                // Swallow
1157
            }
1158
 
1159
            publishParams.setProperty("application_package_name", packageName);
1160
 
1161
            request.setGraphObject(publishParams);
1162
 
1163
            Bundle requestParameters = request.getParameters();
1164
            if (requestParameters == null) {
1165
                requestParameters = new Bundle();
1166
            }
1167
 
1168
            String jsonString = events.toString();
1169
            if (jsonString != null) {
1170
                requestParameters.putByteArray("custom_events_file", getStringAsByteArray(jsonString));
1171
                request.setTag(jsonString);
1172
            }
1173
            request.setParameters(requestParameters);
1174
        }
1175
 
1176
        private byte[] getStringAsByteArray(String jsonString) {
1177
            byte[] jsonUtf8 = null;
1178
            try {
1179
                jsonUtf8 = jsonString.getBytes("UTF-8");
1180
            } catch (UnsupportedEncodingException e) {
1181
                // shouldn't happen, but just in case:
1182
                Utility.logd("Encoding exception: ", e);
1183
            }
1184
            return jsonUtf8;
1185
        }
1186
    }
1187
 
1188
    static class AppEvent implements Serializable {
1189
        private static final long serialVersionUID = 1L;
1190
 
1191
        private JSONObject jsonObject;
1192
        private boolean isImplicit;
1193
        private static final HashSet<String> validatedIdentifiers = new HashSet<String>();
1194
        private String name;
1195
 
1196
        public AppEvent(
1197
                Context context,
1198
                String eventName,
1199
                Double valueToSum,
1200
                Bundle parameters,
1201
                boolean isImplicitlyLogged
1202
        ) {
1203
            try {
1204
                validateIdentifier(eventName);
1205
 
1206
                this.name = eventName;
1207
                isImplicit = isImplicitlyLogged;
1208
                jsonObject = new JSONObject();
1209
 
1210
                jsonObject.put("_eventName", eventName);
1211
                jsonObject.put("_logTime", System.currentTimeMillis() / 1000);
1212
                jsonObject.put("_ui", Utility.getActivityName(context));
1213
 
1214
                if (valueToSum != null) {
1215
                    jsonObject.put("_valueToSum", valueToSum.doubleValue());
1216
                }
1217
 
1218
                if (isImplicit) {
1219
                    jsonObject.put("_implicitlyLogged", "1");
1220
                }
1221
 
1222
                String appVersion = Settings.getAppVersion();
1223
                if (appVersion != null) {
1224
                    jsonObject.put("_appVersion", appVersion);
1225
                }
1226
 
1227
                if (parameters != null) {
1228
                    for (String key : parameters.keySet()) {
1229
 
1230
                        validateIdentifier(key);
1231
 
1232
                        Object value = parameters.get(key);
1233
                        if (!(value instanceof String) && !(value instanceof Number)) {
1234
                            throw new FacebookException(
1235
                                    String.format(
1236
                                            "Parameter value '%s' for key '%s' should be a string or a numeric type.",
1237
                                            value,
1238
                                            key)
1239
                            );
1240
                        }
1241
 
1242
                        jsonObject.put(key, value.toString());
1243
                    }
1244
                }
1245
 
1246
                if (!isImplicit) {
1247
                    Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
1248
                            "Created app event '%s'", jsonObject.toString());
1249
                }
1250
            } catch (JSONException jsonException) {
1251
 
1252
                // If any of the above failed, just consider this an illegal event.
1253
                Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
1254
                        "JSON encoding for app event failed: '%s'", jsonException.toString());
1255
                jsonObject = null;
1256
 
1257
            } catch (FacebookException e) {
1258
                // If any of the above failed, just consider this an illegal event.
1259
                Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents",
1260
                        "Invalid app event name or parameter:", e.toString());
1261
                jsonObject = null;
1262
            }
1263
        }
1264
 
1265
        public String getName() {
1266
            return name;
1267
        }
1268
 
1269
        private AppEvent(String jsonString, boolean isImplicit) throws JSONException {
1270
            jsonObject = new JSONObject(jsonString);
1271
            this.isImplicit = isImplicit;
1272
        }
1273
 
1274
        public boolean getIsImplicit() {
1275
            return isImplicit;
1276
        }
1277
 
1278
        public JSONObject getJSONObject() {
1279
            return jsonObject;
1280
        }
1281
 
1282
        // throw exception if not valid.
1283
        private void validateIdentifier(String identifier) throws FacebookException {
1284
 
1285
            // Identifier should be 40 chars or less, and only have 0-9A-Za-z, underscore, hyphen, and space (but no
1286
            // hyphen or space in the first position).
1287
            final String regex = "^[0-9a-zA-Z_]+[0-9a-zA-Z _-]*$";
1288
 
1289
            final int MAX_IDENTIFIER_LENGTH = 40;
1290
            if (identifier == null || identifier.length() == 0 || identifier.length() > MAX_IDENTIFIER_LENGTH) {
1291
                if (identifier == null) {
1292
                    identifier = "<None Provided>";
1293
                }
1294
                throw new FacebookException(
1295
                    String.format("Identifier '%s' must be less than %d characters", identifier, MAX_IDENTIFIER_LENGTH)
1296
                );
1297
            }
1298
 
1299
            boolean alreadyValidated = false;
1300
            synchronized (validatedIdentifiers) {
1301
                alreadyValidated = validatedIdentifiers.contains(identifier);
1302
            }
1303
 
1304
            if (!alreadyValidated) {
1305
                if (identifier.matches(regex)) {
1306
                    synchronized (validatedIdentifiers) {
1307
                        validatedIdentifiers.add(identifier);
1308
                    }
1309
                } else {
1310
                    throw new FacebookException(
1311
                            String.format("Skipping event named '%s' due to illegal name - must be under 40 chars " +
1312
                                            "and alphanumeric, _, - or space, and not start with a space or hyphen.",
1313
                                    identifier
1314
                            )
1315
                    );
1316
                }
1317
            }
1318
        }
1319
 
1320
        private static class SerializationProxyV1 implements Serializable {
1321
            private static final long serialVersionUID = -2488473066578201069L;
1322
            private final String jsonString;
1323
            private final boolean isImplicit;
1324
 
1325
            private SerializationProxyV1(String jsonString, boolean isImplicit) {
1326
                this.jsonString = jsonString;
1327
                this.isImplicit = isImplicit;
1328
            }
1329
 
1330
            private Object readResolve() throws JSONException {
1331
                return new AppEvent(jsonString, isImplicit);
1332
            }
1333
        }
1334
 
1335
        private Object writeReplace() {
1336
            return new SerializationProxyV1(jsonObject.toString(), isImplicit);
1337
        }
1338
 
1339
        @Override
1340
        public String toString() {
1341
            return String.format("\"%s\", implicit: %b, json: %s", jsonObject.optString("_eventName"),
1342
                    isImplicit, jsonObject.toString());
1343
        }
1344
    }
1345
 
1346
    static class PersistedAppSessionInfo {
1347
        private static final String PERSISTED_SESSION_INFO_FILENAME =
1348
                "AppEventsLogger.persistedsessioninfo";
1349
 
1350
        private static final Object staticLock = new Object();
1351
        private static boolean hasChanges = false;
1352
        private static boolean isLoaded = false;
1353
        private static Map<AccessTokenAppIdPair, FacebookTimeSpentData> appSessionInfoMap;
1354
 
1355
        private static final Runnable appSessionInfoFlushRunnable = new Runnable() {
1356
            @Override
1357
            public void run() {
1358
                PersistedAppSessionInfo.saveAppSessionInformation(applicationContext);
1359
            }
1360
        };
1361
 
1362
        @SuppressWarnings("unchecked")
1363
        private static void restoreAppSessionInformation(Context context) {
1364
            ObjectInputStream ois = null;
1365
 
1366
            synchronized (staticLock) {
1367
                if (!isLoaded) {
1368
                    try {
1369
                        ois =
1370
                                new ObjectInputStream(
1371
                                        context.openFileInput(PERSISTED_SESSION_INFO_FILENAME));
1372
                        appSessionInfoMap =
1373
                                (HashMap<AccessTokenAppIdPair, FacebookTimeSpentData>) ois.readObject();
1374
                        Logger.log(
1375
                                LoggingBehavior.APP_EVENTS,
1376
                                "AppEvents",
1377
                                "App session info loaded");
1378
                    } catch (FileNotFoundException fex) {
1379
                    } catch (Exception e) {
1380
                        Log.d(TAG, "Got unexpected exception: " + e.toString());
1381
                    } finally {
1382
                        Utility.closeQuietly(ois);
1383
                        context.deleteFile(PERSISTED_SESSION_INFO_FILENAME);
1384
                        if (appSessionInfoMap == null) {
1385
                            appSessionInfoMap =
1386
                                    new HashMap<AccessTokenAppIdPair, FacebookTimeSpentData>();
1387
                        }
1388
                        // Regardless of the outcome of the load, the session information cache
1389
                        // is always deleted. Therefore, always treat the session information cache
1390
                        // as loaded
1391
                        isLoaded = true;
1392
                        hasChanges = false;
1393
                    }
1394
                }
1395
            }
1396
        }
1397
 
1398
        static void saveAppSessionInformation(Context context) {
1399
            ObjectOutputStream oos = null;
1400
 
1401
            synchronized (staticLock) {
1402
                if (hasChanges) {
1403
                    try {
1404
                        oos = new ObjectOutputStream(
1405
                                new BufferedOutputStream(
1406
                                        context.openFileOutput(
1407
                                                PERSISTED_SESSION_INFO_FILENAME,
1408
                                                Context.MODE_PRIVATE)
1409
                                )
1410
                        );
1411
                        oos.writeObject(appSessionInfoMap);
1412
                        hasChanges = false;
1413
                        Logger.log(LoggingBehavior.APP_EVENTS, "AppEvents", "App session info saved");
1414
                    } catch (Exception e) {
1415
                        Log.d(TAG, "Got unexpected exception: " + e.toString());
1416
                    } finally {
1417
                        Utility.closeQuietly(oos);
1418
                    }
1419
                }
1420
            }
1421
        }
1422
 
1423
        static void onResume(
1424
                Context context,
1425
                AccessTokenAppIdPair accessTokenAppId,
1426
                AppEventsLogger logger,
1427
                long eventTime,
1428
                String sourceApplicationInfo
1429
        ) {
1430
            synchronized (staticLock) {
1431
                FacebookTimeSpentData timeSpentData = getTimeSpentData(context, accessTokenAppId);
1432
                timeSpentData.onResume(logger, eventTime, sourceApplicationInfo);
1433
                onTimeSpentDataUpdate();
1434
            }
1435
        }
1436
 
1437
        static void onSuspend(
1438
                Context context,
1439
                AccessTokenAppIdPair accessTokenAppId,
1440
                AppEventsLogger logger,
1441
                long eventTime
1442
        ) {
1443
            synchronized (staticLock) {
1444
                FacebookTimeSpentData timeSpentData = getTimeSpentData(context, accessTokenAppId);
1445
                timeSpentData.onSuspend(logger, eventTime);
1446
                onTimeSpentDataUpdate();
1447
            }
1448
        }
1449
 
1450
        private static FacebookTimeSpentData getTimeSpentData(
1451
                Context context,
1452
                AccessTokenAppIdPair accessTokenAppId
1453
        ) {
1454
            restoreAppSessionInformation(context);
1455
            FacebookTimeSpentData result = null;
1456
 
1457
            result = appSessionInfoMap.get(accessTokenAppId);
1458
            if (result == null) {
1459
                result = new FacebookTimeSpentData();
1460
                appSessionInfoMap.put(accessTokenAppId, result);
1461
            }
1462
 
1463
            return result;
1464
        }
1465
 
1466
        private static void onTimeSpentDataUpdate() {
1467
            if (!hasChanges) {
1468
                hasChanges = true;
1469
                backgroundExecutor.schedule(
1470
                        appSessionInfoFlushRunnable,
1471
                        FLUSH_APP_SESSION_INFO_IN_SECONDS,
1472
                        TimeUnit.SECONDS);
1473
            }
1474
        }
1475
    }
1476
 
1477
    // Read/write operations are thread-safe/atomic across all instances of PersistedEvents, but modifications
1478
    // to any individual instance are not thread-safe.
1479
    static class PersistedEvents {
1480
        static final String PERSISTED_EVENTS_FILENAME = "AppEventsLogger.persistedevents";
1481
 
1482
        private static Object staticLock = new Object();
1483
 
1484
        private Context context;
1485
        private HashMap<AccessTokenAppIdPair, List<AppEvent>> persistedEvents =
1486
                new HashMap<AccessTokenAppIdPair, List<AppEvent>>();
1487
 
1488
        private PersistedEvents(Context context) {
1489
            this.context = context;
1490
        }
1491
 
1492
        public static PersistedEvents readAndClearStore(Context context) {
1493
            synchronized (staticLock) {
1494
                PersistedEvents persistedEvents = new PersistedEvents(context);
1495
 
1496
                persistedEvents.readAndClearStore();
1497
 
1498
                return persistedEvents;
1499
            }
1500
        }
1501
 
1502
        public static void persistEvents(Context context, AccessTokenAppIdPair accessTokenAppId,
1503
                                         SessionEventsState eventsToPersist) {
1504
            Map<AccessTokenAppIdPair, SessionEventsState> map = new HashMap<AccessTokenAppIdPair, SessionEventsState>();
1505
            map.put(accessTokenAppId, eventsToPersist);
1506
            persistEvents(context, map);
1507
        }
1508
 
1509
        public static void persistEvents(Context context,
1510
                                         Map<AccessTokenAppIdPair, SessionEventsState> eventsToPersist) {
1511
            synchronized (staticLock) {
1512
                // Note that we don't track which instance of AppEventsLogger added a particular event to
1513
                // SessionEventsState; when a particular Context is being destroyed, we'll persist all accumulated
1514
                // events. More sophisticated tracking could be done to try to reduce unnecessary persisting of events,
1515
                // but the overall number of events is not expected to be large.
1516
                PersistedEvents persistedEvents = readAndClearStore(context);
1517
 
1518
                for (Map.Entry<AccessTokenAppIdPair, SessionEventsState> entry : eventsToPersist.entrySet()) {
1519
                    List<AppEvent> events = entry.getValue().getEventsToPersist();
1520
                    if (events.size() == 0) {
1521
                        continue;
1522
                    }
1523
 
1524
                    persistedEvents.addEvents(entry.getKey(), events);
1525
                }
1526
 
1527
                persistedEvents.write();
1528
            }
1529
        }
1530
 
1531
        public Set<AccessTokenAppIdPair> keySet() {
1532
            return persistedEvents.keySet();
1533
        }
1534
 
1535
        public List<AppEvent> getEvents(AccessTokenAppIdPair accessTokenAppId) {
1536
            return persistedEvents.get(accessTokenAppId);
1537
        }
1538
 
1539
        private void write() {
1540
            ObjectOutputStream oos = null;
1541
            try {
1542
                oos = new ObjectOutputStream(
1543
                        new BufferedOutputStream(context.openFileOutput(PERSISTED_EVENTS_FILENAME, 0)));
1544
                oos.writeObject(persistedEvents);
1545
            } catch (Exception e) {
1546
                Log.d(TAG, "Got unexpected exception: " + e.toString());
1547
            } finally {
1548
                Utility.closeQuietly(oos);
1549
            }
1550
        }
1551
 
1552
        private void readAndClearStore() {
1553
            ObjectInputStream ois = null;
1554
            try {
1555
                ois = new ObjectInputStream(
1556
                        new BufferedInputStream(context.openFileInput(PERSISTED_EVENTS_FILENAME)));
1557
 
1558
                @SuppressWarnings("unchecked")
1559
                HashMap<AccessTokenAppIdPair, List<AppEvent>> obj =
1560
                        (HashMap<AccessTokenAppIdPair, List<AppEvent>>) ois.readObject();
1561
 
1562
                // Note: We delete the store before we store the events; this means we'd prefer to lose some
1563
                // events in the case of exception rather than potentially log them twice.
1564
                context.getFileStreamPath(PERSISTED_EVENTS_FILENAME).delete();
1565
                persistedEvents = obj;
1566
            } catch (FileNotFoundException e) {
1567
                // Expected if we never persisted any events.
1568
            } catch (Exception e) {
1569
                Log.d(TAG, "Got unexpected exception: " + e.toString());
1570
            } finally {
1571
                Utility.closeQuietly(ois);
1572
            }
1573
        }
1574
 
1575
        public void addEvents(AccessTokenAppIdPair accessTokenAppId, List<AppEvent> eventsToPersist) {
1576
            if (!persistedEvents.containsKey(accessTokenAppId)) {
1577
                persistedEvents.put(accessTokenAppId, new ArrayList<AppEvent>());
1578
            }
1579
            persistedEvents.get(accessTokenAppId).addAll(eventsToPersist);
1580
        }
1581
    }
1582
}