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.content.Context;
20
import android.content.SharedPreferences;
21
import android.content.pm.PackageInfo;
22
import android.content.pm.PackageManager;
23
import android.net.Uri;
24
import android.os.AsyncTask;
25
import android.os.Bundle;
26
import android.os.Parcelable;
27
import android.provider.Settings.Secure;
28
import android.text.TextUtils;
29
import android.util.Log;
30
import android.webkit.CookieManager;
31
import android.webkit.CookieSyncManager;
32
import com.facebook.FacebookException;
33
import com.facebook.Request;
34
import com.facebook.Settings;
35
import com.facebook.model.GraphObject;
36
import org.json.JSONArray;
37
import org.json.JSONException;
38
import org.json.JSONObject;
39
import org.json.JSONTokener;
40
 
41
import java.io.*;
42
import java.lang.reflect.InvocationTargetException;
43
import java.lang.reflect.Method;
44
import java.net.HttpURLConnection;
45
import java.net.URLConnection;
46
import java.net.URLDecoder;
47
import java.security.MessageDigest;
48
import java.security.NoSuchAlgorithmException;
49
import java.util.*;
50
import java.util.concurrent.ConcurrentHashMap;
51
 
52
/**
53
 * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of
54
 * any of the classes in this package is unsupported, and they may be modified or removed without warning at
55
 * any time.
56
 */
57
public final class Utility {
58
    static final String LOG_TAG = "FacebookSDK";
59
    private static final String HASH_ALGORITHM_MD5 = "MD5";
60
    private static final String HASH_ALGORITHM_SHA1 = "SHA-1";
61
    private static final String URL_SCHEME = "https";
62
    private static final String APP_SETTINGS_PREFS_STORE = "com.facebook.internal.preferences.APP_SETTINGS";
63
    private static final String APP_SETTINGS_PREFS_KEY_FORMAT = "com.facebook.internal.APP_SETTINGS.%s";
64
    private static final String APP_SETTING_SUPPORTS_ATTRIBUTION = "supports_attribution";
65
    private static final String APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING = "supports_implicit_sdk_logging";
66
    private static final String APP_SETTING_NUX_CONTENT = "gdpv4_nux_content";
67
    private static final String APP_SETTING_NUX_ENABLED = "gdpv4_nux_enabled";
68
    private static final String APP_SETTING_DIALOG_CONFIGS = "android_dialog_configs";
69
    private static final String EXTRA_APP_EVENTS_INFO_FORMAT_VERSION = "a1";
70
    private static final String DIALOG_CONFIG_DIALOG_NAME_FEATURE_NAME_SEPARATOR = "\\|";
71
    private static final String DIALOG_CONFIG_NAME_KEY = "name";
72
    private static final String DIALOG_CONFIG_VERSIONS_KEY = "versions";
73
    private static final String DIALOG_CONFIG_URL_KEY = "url";
74
 
75
    private final static String UTF8 = "UTF-8";
76
 
77
    private static final String[] APP_SETTING_FIELDS = new String[] {
78
            APP_SETTING_SUPPORTS_ATTRIBUTION,
79
            APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING,
80
            APP_SETTING_NUX_CONTENT,
81
            APP_SETTING_NUX_ENABLED,
82
            APP_SETTING_DIALOG_CONFIGS
83
    };
84
    private static final String APPLICATION_FIELDS = "fields";
85
 
86
    // This is the default used by the buffer streams, but they trace a warning if you do not specify.
87
    public static final int DEFAULT_STREAM_BUFFER_SIZE = 8192;
88
 
89
    private static Map<String, FetchedAppSettings> fetchedAppSettings =
90
            new ConcurrentHashMap<String, FetchedAppSettings>();
91
 
92
    private static AsyncTask<Void, Void, GraphObject> initialAppSettingsLoadTask;
93
 
94
    public static class FetchedAppSettings {
95
        private boolean supportsAttribution;
96
        private boolean supportsImplicitLogging;
97
        private String nuxContent;
98
        private boolean nuxEnabled;
99
        private Map<String, Map<String, DialogFeatureConfig>> dialogConfigMap;
100
 
101
        private FetchedAppSettings(boolean supportsAttribution,
102
                                   boolean supportsImplicitLogging,
103
                                   String nuxContent,
104
                                   boolean nuxEnabled,
105
                                   Map<String, Map<String, DialogFeatureConfig>> dialogConfigMap) {
106
            this.supportsAttribution = supportsAttribution;
107
            this.supportsImplicitLogging = supportsImplicitLogging;
108
            this.nuxContent = nuxContent;
109
            this.nuxEnabled = nuxEnabled;
110
            this.dialogConfigMap = dialogConfigMap;
111
        }
112
 
113
        public boolean supportsAttribution() {
114
            return supportsAttribution;
115
        }
116
 
117
        public boolean supportsImplicitLogging() {
118
            return supportsImplicitLogging;
119
        }
120
 
121
        public String getNuxContent() {
122
            return nuxContent;
123
        }
124
 
125
        public boolean getNuxEnabled() {
126
            return nuxEnabled;
127
        }
128
 
129
        public Map<String, Map<String, DialogFeatureConfig>> getDialogConfigurations() {
130
            return dialogConfigMap;
131
        }
132
    }
133
 
134
    public static class DialogFeatureConfig {
135
        private static DialogFeatureConfig parseDialogConfig(JSONObject dialogConfigJSON) {
136
            String dialogNameWithFeature = dialogConfigJSON.optString(DIALOG_CONFIG_NAME_KEY);
137
            if (Utility.isNullOrEmpty(dialogNameWithFeature)) {
138
                return null;
139
            }
140
 
141
            String[] components = dialogNameWithFeature.split(DIALOG_CONFIG_DIALOG_NAME_FEATURE_NAME_SEPARATOR);
142
            if (components.length != 2) {
143
                // We expect the format to be dialogName|FeatureName, where both components are non-empty.
144
                return null;
145
            }
146
 
147
            String dialogName = components[0];
148
            String featureName = components[1];
149
            if (isNullOrEmpty(dialogName) || isNullOrEmpty(featureName)) {
150
                return null;
151
            }
152
 
153
            String urlString = dialogConfigJSON.optString(DIALOG_CONFIG_URL_KEY);
154
            Uri fallbackUri = null;
155
            if (!Utility.isNullOrEmpty(urlString)) {
156
                fallbackUri = Uri.parse(urlString);
157
            }
158
 
159
            JSONArray versionsJSON = dialogConfigJSON.optJSONArray(DIALOG_CONFIG_VERSIONS_KEY);
160
 
161
            int[] featureVersionSpec = parseVersionSpec(versionsJSON);
162
 
163
            return new DialogFeatureConfig(dialogName, featureName, fallbackUri, featureVersionSpec);
164
        }
165
 
166
        private static int[] parseVersionSpec(JSONArray versionsJSON) {
167
            // Null signifies no overrides to the min-version as specified by the SDK.
168
            // An empty array would basically turn off the dialog (i.e no supported versions), so DON'T default to that.
169
            int[] versionSpec = null;
170
            if (versionsJSON != null) {
171
                int numVersions = versionsJSON.length();
172
                versionSpec = new int[numVersions];
173
                for (int i = 0; i < numVersions; i++) {
174
                    // See if the version was stored directly as an Integer
175
                    int version = versionsJSON.optInt(i, NativeProtocol.NO_PROTOCOL_AVAILABLE);
176
                    if (version == NativeProtocol.NO_PROTOCOL_AVAILABLE) {
177
                        // If not, then see if it was stored as a string that can be parsed out.
178
                        // If even that fails, then we will leave it as NO_PROTOCOL_AVAILABLE
179
                        String versionString = versionsJSON.optString(i);
180
                        if (!isNullOrEmpty(versionString)) {
181
                            try {
182
                                version = Integer.parseInt(versionString);
183
                            } catch (NumberFormatException nfe) {
184
                                logd(LOG_TAG, nfe);
185
                                version = NativeProtocol.NO_PROTOCOL_AVAILABLE;
186
                            }
187
                        }
188
                    }
189
 
190
                    versionSpec[i] = version;
191
                }
192
            }
193
 
194
            return versionSpec;
195
        }
196
 
197
        private String dialogName;
198
        private String featureName;
199
        private Uri fallbackUrl;
200
        private int[] featureVersionSpec;
201
 
202
        private DialogFeatureConfig(String dialogName, String featureName, Uri fallbackUrl, int[] featureVersionSpec) {
203
            this.dialogName = dialogName;
204
            this.featureName = featureName;
205
            this.fallbackUrl = fallbackUrl;
206
            this.featureVersionSpec = featureVersionSpec;
207
        }
208
 
209
        public String getDialogName() {
210
            return dialogName;
211
        }
212
 
213
        public String getFeatureName() {
214
            return featureName;
215
        }
216
 
217
        public Uri getFallbackUrl() {
218
            return fallbackUrl;
219
        }
220
 
221
        public int[] getVersionSpec() {
222
            return featureVersionSpec;
223
        }
224
    }
225
 
226
    /**
227
     * Each array represents a set of closed or open Range, like so:
228
     * [0,10,50,60] - Ranges are {0-9}, {50-59}
229
     * [20] - Ranges are {20-}
230
     * [30,40,100] - Ranges are {30-39}, {100-}
231
     *
232
     * All Ranges in the array have a closed lower bound. Only the last Range in each array may be open.
233
     * It is assumed that the passed in arrays are sorted with ascending order.
234
     * It is assumed that no two elements in a given are equal (i.e. no 0-length ranges)
235
     *
236
     * The method returns an intersect of the two passed in Range-sets
237
     * @param range1
238
     * @param range2
239
     * @return
240
     */
241
    public static int[] intersectRanges(int[] range1, int[] range2) {
242
        if (range1 == null) {
243
            return range2;
244
        } else if (range2 == null) {
245
            return range1;
246
        }
247
 
248
        int[] outputRange = new int[range1.length + range2.length];
249
        int outputIndex = 0;
250
        int index1 = 0, lower1, upper1;
251
        int index2 = 0, lower2, upper2;
252
        while (index1 < range1.length && index2 < range2.length) {
253
            int newRangeLower = Integer.MIN_VALUE, newRangeUpper = Integer.MAX_VALUE;
254
            lower1 = range1[index1];
255
            upper1 = Integer.MAX_VALUE;
256
 
257
            lower2 = range2[index2];
258
            upper2 = Integer.MAX_VALUE;
259
 
260
            if (index1 < range1.length - 1) {
261
                upper1 = range1[index1 + 1];
262
            }
263
            if (index2 < range2.length - 1) {
264
                upper2 = range2[index2 + 1];
265
            }
266
 
267
            if (lower1 < lower2) {
268
                if (upper1 > lower2) {
269
                    newRangeLower = lower2;
270
                    if (upper1 > upper2) {
271
                        newRangeUpper = upper2;
272
                        index2 += 2;
273
                    } else {
274
                        newRangeUpper = upper1;
275
                        index1 += 2;
276
                    }
277
                } else {
278
                    index1 += 2;
279
                }
280
            } else {
281
                if (upper2 > lower1) {
282
                    newRangeLower = lower1;
283
                    if (upper2 > upper1) {
284
                        newRangeUpper = upper1;
285
                        index1 += 2;
286
                    } else {
287
                        newRangeUpper = upper2;
288
                        index2 += 2;
289
                    }
290
                } else {
291
                    index2 += 2;
292
                }
293
            }
294
 
295
            if (newRangeLower != Integer.MIN_VALUE) {
296
                outputRange[outputIndex ++] = newRangeLower;
297
                if (newRangeUpper != Integer.MAX_VALUE) {
298
                    outputRange[outputIndex ++] = newRangeUpper;
299
                } else {
300
                    // If we reach an unbounded/open range, then we know we're done.
301
                    break;
302
                }
303
            }
304
        }
305
 
306
        return Arrays.copyOf(outputRange, outputIndex);
307
    }
308
 
309
    // Returns true iff all items in subset are in superset, treating null and
310
    // empty collections as
311
    // the same.
312
    public static <T> boolean isSubset(Collection<T> subset, Collection<T> superset) {
313
        if ((superset == null) || (superset.size() == 0)) {
314
            return ((subset == null) || (subset.size() == 0));
315
        }
316
 
317
        HashSet<T> hash = new HashSet<T>(superset);
318
        for (T t : subset) {
319
            if (!hash.contains(t)) {
320
                return false;
321
            }
322
        }
323
        return true;
324
    }
325
 
326
    public static <T> boolean isNullOrEmpty(Collection<T> c) {
327
        return (c == null) || (c.size() == 0);
328
    }
329
 
330
    public static boolean isNullOrEmpty(String s) {
331
        return (s == null) || (s.length() == 0);
332
    }
333
 
334
    /**
335
     * Use this when you want to normalize empty and null strings
336
     * This way, Utility.areObjectsEqual can used for comparison, where a null string is to be treated the same as
337
     * an empty string.
338
     *
339
     * @param s
340
     * @param valueIfNullOrEmpty
341
     * @return
342
     */
343
    public static String coerceValueIfNullOrEmpty(String s, String valueIfNullOrEmpty) {
344
        if (isNullOrEmpty(s)) {
345
            return valueIfNullOrEmpty;
346
        }
347
 
348
        return s;
349
    }
350
 
351
    public static <T> Collection<T> unmodifiableCollection(T... ts) {
352
        return Collections.unmodifiableCollection(Arrays.asList(ts));
353
    }
354
 
355
    public static <T> ArrayList<T> arrayList(T... ts) {
356
        ArrayList<T> arrayList = new ArrayList<T>(ts.length);
357
        for (T t : ts) {
358
            arrayList.add(t);
359
        }
360
        return arrayList;
361
    }
362
 
363
    static String md5hash(String key) {
364
        return hashWithAlgorithm(HASH_ALGORITHM_MD5, key);
365
    }
366
 
367
    static String sha1hash(String key) {
368
        return hashWithAlgorithm(HASH_ALGORITHM_SHA1, key);
369
    }
370
 
371
    static String sha1hash(byte[] bytes) {
372
        return hashWithAlgorithm(HASH_ALGORITHM_SHA1, bytes);
373
    }
374
 
375
    private static String hashWithAlgorithm(String algorithm, String key) {
376
        return hashWithAlgorithm(algorithm, key.getBytes());
377
    }
378
 
379
    private static String hashWithAlgorithm(String algorithm, byte[] bytes) {
380
        MessageDigest hash;
381
        try {
382
            hash = MessageDigest.getInstance(algorithm);
383
        } catch (NoSuchAlgorithmException e) {
384
            return null;
385
        }
386
        return hashBytes(hash, bytes);
387
    }
388
 
389
    private static String hashBytes(MessageDigest hash, byte[] bytes) {
390
        hash.update(bytes);
391
        byte[] digest = hash.digest();
392
        StringBuilder builder = new StringBuilder();
393
        for (int b : digest) {
394
            builder.append(Integer.toHexString((b >> 4) & 0xf));
395
            builder.append(Integer.toHexString((b >> 0) & 0xf));
396
        }
397
        return builder.toString();
398
    }
399
 
400
    public static Uri buildUri(String authority, String path, Bundle parameters) {
401
        Uri.Builder builder = new Uri.Builder();
402
        builder.scheme(URL_SCHEME);
403
        builder.authority(authority);
404
        builder.path(path);
405
        for (String key : parameters.keySet()) {
406
            Object parameter = parameters.get(key);
407
            if (parameter instanceof String) {
408
                builder.appendQueryParameter(key, (String) parameter);
409
            }
410
        }
411
        return builder.build();
412
    }
413
 
414
    public static Bundle parseUrlQueryString(String queryString) {
415
        Bundle params = new Bundle();
416
        if (!isNullOrEmpty(queryString)) {
417
            String array[] = queryString.split("&");
418
            for (String parameter : array) {
419
                String keyValuePair[] = parameter.split("=");
420
 
421
                try {
422
                    if (keyValuePair.length == 2) {
423
                        params.putString(
424
                                URLDecoder.decode(keyValuePair[0], UTF8),
425
                                URLDecoder.decode(keyValuePair[1], UTF8));
426
                    } else if (keyValuePair.length == 1) {
427
                        params.putString(
428
                                URLDecoder.decode(keyValuePair[0], UTF8),
429
                                "");
430
                    }
431
                } catch (UnsupportedEncodingException e) {
432
                    // shouldn't happen
433
                    logd(LOG_TAG, e);
434
                }
435
            }
436
        }
437
        return params;
438
    }
439
 
440
    public static void putObjectInBundle(Bundle bundle, String key, Object value) {
441
        if (value instanceof String) {
442
            bundle.putString(key, (String) value);
443
        } else if (value instanceof Parcelable) {
444
            bundle.putParcelable(key, (Parcelable) value);
445
        } else if (value instanceof byte[]) {
446
            bundle.putByteArray(key, (byte[]) value);
447
        } else {
448
            throw new FacebookException("attempted to add unsupported type to Bundle");
449
        }
450
    }
451
 
452
    public static void closeQuietly(Closeable closeable) {
453
        try {
454
            if (closeable != null) {
455
                closeable.close();
456
            }
457
        } catch (IOException ioe) {
458
            // ignore
459
        }
460
    }
461
 
462
    public static void disconnectQuietly(URLConnection connection) {
463
        if (connection instanceof HttpURLConnection) {
464
            ((HttpURLConnection) connection).disconnect();
465
        }
466
    }
467
 
468
    public static String getMetadataApplicationId(Context context) {
469
        Validate.notNull(context, "context");
470
 
471
        Settings.loadDefaultsFromMetadata(context);
472
 
473
        return Settings.getApplicationId();
474
    }
475
 
476
    static Map<String, Object> convertJSONObjectToHashMap(JSONObject jsonObject) {
477
        HashMap<String, Object> map = new HashMap<String, Object>();
478
        JSONArray keys = jsonObject.names();
479
        for (int i = 0; i < keys.length(); ++i) {
480
            String key;
481
            try {
482
                key = keys.getString(i);
483
                Object value = jsonObject.get(key);
484
                if (value instanceof JSONObject) {
485
                    value = convertJSONObjectToHashMap((JSONObject) value);
486
                }
487
                map.put(key, value);
488
            } catch (JSONException e) {
489
            }
490
        }
491
        return map;
492
    }
493
 
494
    // Returns either a JSONObject or JSONArray representation of the 'key' property of 'jsonObject'.
495
    public static Object getStringPropertyAsJSON(JSONObject jsonObject, String key, String nonJSONPropertyKey)
496
            throws JSONException {
497
        Object value = jsonObject.opt(key);
498
        if (value != null && value instanceof String) {
499
            JSONTokener tokener = new JSONTokener((String) value);
500
            value = tokener.nextValue();
501
        }
502
 
503
        if (value != null && !(value instanceof JSONObject || value instanceof JSONArray)) {
504
            if (nonJSONPropertyKey != null) {
505
                // Facebook sometimes gives us back a non-JSON value such as
506
                // literal "true" or "false" as a result.
507
                // If we got something like that, we present it to the caller as
508
                // a GraphObject with a single
509
                // property. We only do this if the caller wants that behavior.
510
                jsonObject = new JSONObject();
511
                jsonObject.putOpt(nonJSONPropertyKey, value);
512
                return jsonObject;
513
            } else {
514
                throw new FacebookException("Got an unexpected non-JSON object.");
515
            }
516
        }
517
 
518
        return value;
519
 
520
    }
521
 
522
    public static String readStreamToString(InputStream inputStream) throws IOException {
523
        BufferedInputStream bufferedInputStream = null;
524
        InputStreamReader reader = null;
525
        try {
526
            bufferedInputStream = new BufferedInputStream(inputStream);
527
            reader = new InputStreamReader(bufferedInputStream);
528
            StringBuilder stringBuilder = new StringBuilder();
529
 
530
            final int bufferSize = 1024 * 2;
531
            char[] buffer = new char[bufferSize];
532
            int n = 0;
533
            while ((n = reader.read(buffer)) != -1) {
534
                stringBuilder.append(buffer, 0, n);
535
            }
536
 
537
            return stringBuilder.toString();
538
        } finally {
539
            closeQuietly(bufferedInputStream);
540
            closeQuietly(reader);
541
        }
542
    }
543
 
544
    public static boolean stringsEqualOrEmpty(String a, String b) {
545
        boolean aEmpty = TextUtils.isEmpty(a);
546
        boolean bEmpty = TextUtils.isEmpty(b);
547
 
548
        if (aEmpty && bEmpty) {
549
            // Both null or empty, they match.
550
            return true;
551
        }
552
        if (!aEmpty && !bEmpty) {
553
            // Both non-empty, check equality.
554
            return a.equals(b);
555
        }
556
        // One empty, one non-empty, can't match.
557
        return false;
558
    }
559
 
560
    private static void clearCookiesForDomain(Context context, String domain) {
561
        // This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager
562
        // has never been created.
563
        CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
564
        syncManager.sync();
565
 
566
        CookieManager cookieManager = CookieManager.getInstance();
567
 
568
        String cookies = cookieManager.getCookie(domain);
569
        if (cookies == null) {
570
            return;
571
        }
572
 
573
        String[] splitCookies = cookies.split(";");
574
        for (String cookie : splitCookies) {
575
            String[] cookieParts = cookie.split("=");
576
            if (cookieParts.length > 0) {
577
                String newCookie = cookieParts[0].trim() + "=;expires=Sat, 1 Jan 2000 00:00:01 UTC;";
578
                cookieManager.setCookie(domain, newCookie);
579
            }
580
        }
581
        cookieManager.removeExpiredCookie();
582
    }
583
 
584
    public static void clearFacebookCookies(Context context) {
585
        // setCookie acts differently when trying to expire cookies between builds of Android that are using
586
        // Chromium HTTP stack and those that are not. Using both of these domains to ensure it works on both.
587
        clearCookiesForDomain(context, "facebook.com");
588
        clearCookiesForDomain(context, ".facebook.com");
589
        clearCookiesForDomain(context, "https://facebook.com");
590
        clearCookiesForDomain(context, "https://.facebook.com");
591
    }
592
 
593
    public static void logd(String tag, Exception e) {
594
        if (Settings.isDebugEnabled() && tag != null && e != null) {
595
            Log.d(tag, e.getClass().getSimpleName() + ": " + e.getMessage());
596
        }
597
    }
598
 
599
    public static void logd(String tag, String msg) {
600
        if (Settings.isDebugEnabled() && tag != null && msg != null) {
601
            Log.d(tag, msg);
602
        }
603
    }
604
 
605
    public static void logd(String tag, String msg, Throwable t) {
606
        if (Settings.isDebugEnabled() && !isNullOrEmpty(tag)) {
607
            Log.d(tag, msg, t);
608
        }
609
    }
610
 
611
    public static <T> boolean areObjectsEqual(T a, T b) {
612
        if (a == null) {
613
            return b == null;
614
        }
615
        return a.equals(b);
616
    }
617
 
618
    public static void loadAppSettingsAsync(final Context context, final String applicationId) {
619
        if (Utility.isNullOrEmpty(applicationId) ||
620
                fetchedAppSettings.containsKey(applicationId) ||
621
                initialAppSettingsLoadTask != null) {
622
            return;
623
        }
624
 
625
        final String settingsKey = String.format(APP_SETTINGS_PREFS_KEY_FORMAT, applicationId);
626
 
627
        initialAppSettingsLoadTask = new AsyncTask<Void, Void, GraphObject>() {
628
            @Override
629
            protected GraphObject doInBackground(Void... params) {
630
                return getAppSettingsQueryResponse(applicationId);
631
            }
632
 
633
            @Override
634
            protected void onPostExecute(GraphObject result) {
635
                if (result != null) {
636
                    JSONObject resultJSON = result.getInnerJSONObject();
637
                    parseAppSettingsFromJSON(applicationId, resultJSON);
638
 
639
                    SharedPreferences sharedPrefs = context.getSharedPreferences(
640
                            APP_SETTINGS_PREFS_STORE,
641
                            Context.MODE_PRIVATE);
642
                    sharedPrefs.edit()
643
                        .putString(settingsKey, resultJSON.toString())
644
                        .apply();
645
                }
646
 
647
                initialAppSettingsLoadTask = null;
648
            }
649
        };
650
        initialAppSettingsLoadTask.execute((Void[])null);
651
 
652
        // Also see if we had a cached copy and use that immediately.
653
        SharedPreferences sharedPrefs = context.getSharedPreferences(
654
                APP_SETTINGS_PREFS_STORE,
655
                Context.MODE_PRIVATE);
656
        String settingsJSONString = sharedPrefs.getString(settingsKey, null);
657
        if (!isNullOrEmpty(settingsJSONString)) {
658
            JSONObject settingsJSON = null;
659
            try {
660
                settingsJSON = new JSONObject(settingsJSONString);
661
            } catch (JSONException je) {
662
                logd(LOG_TAG, je);
663
            }
664
            if (settingsJSON != null) {
665
                parseAppSettingsFromJSON(applicationId, settingsJSON);
666
            }
667
        }
668
    }
669
 
670
    // Note that this method makes a synchronous Graph API call, so should not be called from the main thread.
671
    public static FetchedAppSettings queryAppSettings(final String applicationId, final boolean forceRequery) {
672
        // Cache the last app checked results.
673
        if (!forceRequery && fetchedAppSettings.containsKey(applicationId)) {
674
            return fetchedAppSettings.get(applicationId);
675
        }
676
 
677
        GraphObject response = getAppSettingsQueryResponse(applicationId);
678
        if (response == null) {
679
            return null;
680
        }
681
 
682
        return parseAppSettingsFromJSON(applicationId, response.getInnerJSONObject());
683
    }
684
 
685
    private static FetchedAppSettings parseAppSettingsFromJSON(String applicationId, JSONObject settingsJSON) {
686
        FetchedAppSettings result = new FetchedAppSettings(
687
                settingsJSON.optBoolean(APP_SETTING_SUPPORTS_ATTRIBUTION, false),
688
                settingsJSON.optBoolean(APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING, false),
689
                settingsJSON.optString(APP_SETTING_NUX_CONTENT, ""),
690
                settingsJSON.optBoolean(APP_SETTING_NUX_ENABLED, false),
691
                parseDialogConfigurations(settingsJSON.optJSONObject(APP_SETTING_DIALOG_CONFIGS))
692
        );
693
 
694
        fetchedAppSettings.put(applicationId, result);
695
 
696
        return result;
697
    }
698
 
699
    // Note that this method makes a synchronous Graph API call, so should not be called from the main thread.
700
    private static GraphObject getAppSettingsQueryResponse(String applicationId) {
701
        Bundle appSettingsParams = new Bundle();
702
        appSettingsParams.putString(APPLICATION_FIELDS, TextUtils.join(",", APP_SETTING_FIELDS));
703
 
704
        Request request = Request.newGraphPathRequest(null, applicationId, null);
705
        request.setSkipClientToken(true);
706
        request.setParameters(appSettingsParams);
707
 
708
        GraphObject response = request.executeAndWait().getGraphObject();
709
        return response;
710
    }
711
 
712
    public static DialogFeatureConfig getDialogFeatureConfig(String applicationId, String actionName, String featureName) {
713
        if (Utility.isNullOrEmpty(actionName) || Utility.isNullOrEmpty(featureName)) {
714
            return null;
715
        }
716
 
717
        FetchedAppSettings settings = fetchedAppSettings.get(applicationId);
718
        if (settings != null) {
719
            Map<String, DialogFeatureConfig> featureMap = settings.getDialogConfigurations().get(actionName);
720
            if (featureMap != null) {
721
                return featureMap.get(featureName);
722
            }
723
        }
724
        return null;
725
    }
726
 
727
    private static Map<String, Map<String, DialogFeatureConfig>> parseDialogConfigurations(JSONObject dialogConfigResponse) {
728
        HashMap<String, Map<String, DialogFeatureConfig>> dialogConfigMap = new HashMap<String, Map<String, DialogFeatureConfig>>();
729
 
730
        if (dialogConfigResponse != null) {
731
            JSONArray dialogConfigData = dialogConfigResponse.optJSONArray("data");
732
            if (dialogConfigData != null) {
733
                for (int i = 0; i < dialogConfigData.length(); i++) {
734
                    DialogFeatureConfig dialogConfig = DialogFeatureConfig.parseDialogConfig(dialogConfigData.optJSONObject(i));
735
                    if (dialogConfig == null) {
736
                        continue;
737
                    }
738
 
739
                    String dialogName = dialogConfig.getDialogName();
740
                    Map<String, DialogFeatureConfig> featureMap = dialogConfigMap.get(dialogName);
741
                    if (featureMap == null) {
742
                        featureMap = new HashMap<String, DialogFeatureConfig>();
743
                        dialogConfigMap.put(dialogName, featureMap);
744
                    }
745
                    featureMap.put(dialogConfig.getFeatureName(), dialogConfig);
746
                }
747
            }
748
        }
749
 
750
        return dialogConfigMap;
751
    }
752
 
753
    public static boolean safeGetBooleanFromResponse(GraphObject response, String propertyName) {
754
        Object result = false;
755
        if (response != null) {
756
            result = response.getProperty(propertyName);
757
        }
758
        if (!(result instanceof Boolean)) {
759
            result = false;
760
        }
761
        return (Boolean) result;
762
    }
763
 
764
    public static String safeGetStringFromResponse(GraphObject response, String propertyName) {
765
        Object result = "";
766
        if (response != null) {
767
            result = response.getProperty(propertyName);
768
        }
769
        if (!(result instanceof String)) {
770
            result = "";
771
        }
772
        return (String) result;
773
    }
774
 
775
    public static JSONObject tryGetJSONObjectFromResponse(GraphObject response, String propertyKey) {
776
        if (response == null) {
777
            return null;
778
        }
779
        Object property = response.getProperty(propertyKey);
780
        if (!(property instanceof JSONObject)) {
781
            return null;
782
        }
783
        return (JSONObject) property;
784
    }
785
 
786
    public static JSONArray tryGetJSONArrayFromResponse(GraphObject response, String propertyKey) {
787
        if (response == null) {
788
            return null;
789
        }
790
        Object property = response.getProperty(propertyKey);
791
        if (!(property instanceof JSONArray)) {
792
            return null;
793
        }
794
        return (JSONArray) property;
795
    }
796
 
797
    public static void clearCaches(Context context) {
798
        ImageDownloader.clearCache(context);
799
    }
800
 
801
    public static void deleteDirectory(File directoryOrFile) {
802
        if (!directoryOrFile.exists()) {
803
            return;
804
        }
805
 
806
        if (directoryOrFile.isDirectory()) {
807
            for (File child : directoryOrFile.listFiles()) {
808
                deleteDirectory(child);
809
            }
810
        }
811
        directoryOrFile.delete();
812
    }
813
 
814
    public static <T> List<T> asListNoNulls(T... array) {
815
        ArrayList<T> result = new ArrayList<T>();
816
        for (T t : array) {
817
            if (t != null) {
818
                result.add(t);
819
            }
820
        }
821
        return result;
822
    }
823
 
824
    // Return a hash of the android_id combined with the appid.  Intended to dedupe requests on the server side
825
    // in order to do counting of users unknown to Facebook.  Because we put the appid into the key prior to hashing,
826
    // we cannot do correlation of the same user across multiple apps -- this is intentional.  When we transition to
827
    // the Google advertising ID, we'll get rid of this and always send that up.
828
    public static String getHashedDeviceAndAppID(Context context, String applicationId) {
829
        String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
830
 
831
        if (androidId == null) {
832
            return null;
833
        } else {
834
            return sha1hash(androidId + applicationId);
835
        }
836
    }
837
 
838
    public static void setAppEventAttributionParameters(GraphObject params,
839
                                                        AttributionIdentifiers attributionIdentifiers, String hashedDeviceAndAppId, boolean limitEventUsage) {
840
        // Send attributionID if it exists, otherwise send a hashed device+appid specific value as the advertiser_id.
841
        if (attributionIdentifiers != null && attributionIdentifiers.getAttributionId() != null) {
842
            params.setProperty("attribution", attributionIdentifiers.getAttributionId());
843
        }
844
 
845
        if (attributionIdentifiers != null && attributionIdentifiers.getAndroidAdvertiserId() != null) {
846
            params.setProperty("advertiser_id", attributionIdentifiers.getAndroidAdvertiserId());
847
            params.setProperty("advertiser_tracking_enabled", !attributionIdentifiers.isTrackingLimited());
848
        } else if (hashedDeviceAndAppId != null) {
849
            params.setProperty("advertiser_id", hashedDeviceAndAppId);
850
        }
851
 
852
        params.setProperty("application_tracking_enabled", !limitEventUsage);
853
    }
854
 
855
    public static void setAppEventExtendedDeviceInfoParameters(GraphObject params, Context appContext) {
856
        JSONArray extraInfoArray = new JSONArray();
857
        extraInfoArray.put(EXTRA_APP_EVENTS_INFO_FORMAT_VERSION);
858
 
859
        // Application Manifest info:
860
        String pkgName = appContext.getPackageName();
861
        int versionCode = -1;
862
        String versionName = "";
863
 
864
        try {
865
            PackageInfo pi = appContext.getPackageManager().getPackageInfo(pkgName, 0);
866
            versionCode = pi.versionCode;
867
            versionName = pi.versionName;
868
        } catch (PackageManager.NameNotFoundException e) {
869
            // Swallow
870
        }
871
 
872
        // Application Manifest info:
873
        extraInfoArray.put(pkgName);
874
        extraInfoArray.put(versionCode);
875
        extraInfoArray.put(versionName);
876
 
877
        params.setProperty("extinfo", extraInfoArray.toString());
878
    }
879
 
880
    public static Method getMethodQuietly(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
881
        try {
882
            return clazz.getMethod(methodName, parameterTypes);
883
        } catch (NoSuchMethodException ex) {
884
            return null;
885
        }
886
    }
887
 
888
    public static Method getMethodQuietly(String className, String methodName, Class<?>... parameterTypes) {
889
        try {
890
            Class<?> clazz = Class.forName(className);
891
            return getMethodQuietly(clazz, methodName, parameterTypes);
892
        } catch (ClassNotFoundException ex) {
893
            return null;
894
        }
895
    }
896
 
897
    public static Object invokeMethodQuietly(Object receiver, Method method, Object... args) {
898
        try {
899
            return method.invoke(receiver, args);
900
        } catch (IllegalAccessException ex) {
901
            return null;
902
        } catch (InvocationTargetException ex) {
903
            return null;
904
        }
905
    }
906
 
907
    /**
908
     * Returns the name of the current activity if the context is an activity, otherwise return "unknown"
909
     */
910
    public static String getActivityName(Context context) {
911
        if (context == null) {
912
            return "null";
913
        } else if (context == context.getApplicationContext()) {
914
            return "unknown";
915
        } else {
916
            return context.getClass().getSimpleName();
917
        }
918
    }
919
}