/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import com.facebook.internal.Logger; import com.facebook.internal.Utility; import com.facebook.internal.Validate; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; final class LegacyTokenHelper { /** * The key used by AccessTokenCache to store the token value in the Bundle during * load and save. */ public static final String TOKEN_KEY = "com.facebook.TokenCachingStrategy.Token"; /** * The key used by AccessTokenCache to store the expiration date value in the Bundle * during load and save. */ public static final String EXPIRATION_DATE_KEY = "com.facebook.TokenCachingStrategy.ExpirationDate"; /** * The key used by AccessTokenCache to store the last refresh date value in the * Bundle during load and save. */ public static final String LAST_REFRESH_DATE_KEY = "com.facebook.TokenCachingStrategy.LastRefreshDate"; /** * The key used by AccessTokenCache to store an enum indicating the source of the token * in the Bundle during load and save. */ public static final String TOKEN_SOURCE_KEY = "com.facebook.TokenCachingStrategy.AccessTokenSource"; /** * The key used by AccessTokenCache to store the list of permissions granted by the * token in the Bundle during load and save. */ public static final String PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.Permissions"; /** * The key used by AccessTokenCache to store the list of permissions declined by the user in the token in * the Bundle during load and save. */ public static final String DECLINED_PERMISSIONS_KEY = "com.facebook.TokenCachingStrategy.DeclinedPermissions"; public static final String APPLICATION_ID_KEY = "com.facebook.TokenCachingStrategy.ApplicationId"; private static final long INVALID_BUNDLE_MILLISECONDS = Long.MIN_VALUE; private static final String IS_SSO_KEY = "com.facebook.TokenCachingStrategy.IsSSO"; public static final String DEFAULT_CACHE_KEY = "com.facebook.SharedPreferencesTokenCachingStrategy.DEFAULT_KEY"; private static final String TAG = LegacyTokenHelper.class.getSimpleName(); private static final String JSON_VALUE_TYPE = "valueType"; private static final String JSON_VALUE = "value"; private static final String JSON_VALUE_ENUM_TYPE = "enumType"; private static final String TYPE_BOOLEAN = "bool"; private static final String TYPE_BOOLEAN_ARRAY = "bool[]"; private static final String TYPE_BYTE = "byte"; private static final String TYPE_BYTE_ARRAY = "byte[]"; private static final String TYPE_SHORT = "short"; private static final String TYPE_SHORT_ARRAY = "short[]"; private static final String TYPE_INTEGER = "int"; private static final String TYPE_INTEGER_ARRAY = "int[]"; private static final String TYPE_LONG = "long"; private static final String TYPE_LONG_ARRAY = "long[]"; private static final String TYPE_FLOAT = "float"; private static final String TYPE_FLOAT_ARRAY = "float[]"; private static final String TYPE_DOUBLE = "double"; private static final String TYPE_DOUBLE_ARRAY = "double[]"; private static final String TYPE_CHAR = "char"; private static final String TYPE_CHAR_ARRAY = "char[]"; private static final String TYPE_STRING = "string"; private static final String TYPE_STRING_LIST = "stringList"; private static final String TYPE_ENUM = "enum"; private String cacheKey; private SharedPreferences cache; public LegacyTokenHelper(Context context) { this(context, null); } public LegacyTokenHelper(Context context, String cacheKey) { Validate.notNull(context, "context"); this.cacheKey = Utility.isNullOrEmpty(cacheKey) ? DEFAULT_CACHE_KEY : cacheKey; // If the application context is available, use that. However, if it isn't // available (possibly because of a context that was created manually), use // the passed in context directly. Context applicationContext = context.getApplicationContext(); context = applicationContext != null ? applicationContext : context; this.cache = context.getSharedPreferences( this.cacheKey, Context.MODE_PRIVATE); } public Bundle load() { Bundle settings = new Bundle(); Map<String, ?> allCachedEntries = cache.getAll(); for (String key : allCachedEntries.keySet()) { try { deserializeKey(key, settings); } catch (JSONException e) { // Error in the cache. So consider it corrupted and return null Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error reading cached value for key: '" + key + "' -- " + e); return null; } } return settings; } public void save(Bundle bundle) { Validate.notNull(bundle, "bundle"); SharedPreferences.Editor editor = cache.edit(); for (String key : bundle.keySet()) { try { serializeKey(key, bundle, editor); } catch (JSONException e) { // Error in the bundle. Don't store a partial cache. Logger.log( LoggingBehavior.CACHE, Log.WARN, TAG, "Error processing value for key: '" + key + "' -- " + e); // Bypass the commit and just return. This cancels the entire edit transaction return; } } editor.apply(); } /** * Clears out all token information stored in this cache. */ public void clear() { cache.edit().clear().apply(); } public static boolean hasTokenInformation(Bundle bundle) { if (bundle == null) { return false; } String token = bundle.getString(TOKEN_KEY); if ((token == null) || (token.length() == 0)) { return false; } long expiresMilliseconds = bundle.getLong(EXPIRATION_DATE_KEY, 0L); if (expiresMilliseconds == 0L) { return false; } return true; } public static String getToken(Bundle bundle) { Validate.notNull(bundle, "bundle"); return bundle.getString(TOKEN_KEY); } public static void putToken(Bundle bundle, String value) { Validate.notNull(bundle, "bundle"); Validate.notNull(value, "value"); bundle.putString(TOKEN_KEY, value); } public static Date getExpirationDate(Bundle bundle) { Validate.notNull(bundle, "bundle"); return getDate(bundle, EXPIRATION_DATE_KEY); } public static void putExpirationDate(Bundle bundle, Date value) { Validate.notNull(bundle, "bundle"); Validate.notNull(value, "value"); putDate(bundle, EXPIRATION_DATE_KEY, value); } public static long getExpirationMilliseconds(Bundle bundle) { Validate.notNull(bundle, "bundle"); return bundle.getLong(EXPIRATION_DATE_KEY); } public static void putExpirationMilliseconds(Bundle bundle, long value) { Validate.notNull(bundle, "bundle"); bundle.putLong(EXPIRATION_DATE_KEY, value); } public static Set<String> getPermissions(Bundle bundle) { Validate.notNull(bundle, "bundle"); ArrayList<String> arrayList = bundle.getStringArrayList(PERMISSIONS_KEY); if (arrayList == null) { return null; } return new HashSet<String>(arrayList); } public static void putPermissions(Bundle bundle, Collection<String> value) { Validate.notNull(bundle, "bundle"); Validate.notNull(value, "value"); bundle.putStringArrayList(PERMISSIONS_KEY, new ArrayList<String>(value)); } public static void putDeclinedPermissions(Bundle bundle, Collection<String> value) { Validate.notNull(bundle, "bundle"); Validate.notNull(value, "value"); bundle.putStringArrayList(DECLINED_PERMISSIONS_KEY, new ArrayList<String>(value)); } public static AccessTokenSource getSource(Bundle bundle) { Validate.notNull(bundle, "bundle"); if (bundle.containsKey(TOKEN_SOURCE_KEY)) { return (AccessTokenSource) bundle.getSerializable(TOKEN_SOURCE_KEY); } else { boolean isSSO = bundle.getBoolean(IS_SSO_KEY); return isSSO ? AccessTokenSource.FACEBOOK_APPLICATION_WEB : AccessTokenSource.WEB_VIEW; } } public static void putSource(Bundle bundle, AccessTokenSource value) { Validate.notNull(bundle, "bundle"); bundle.putSerializable(TOKEN_SOURCE_KEY, value); } public static Date getLastRefreshDate(Bundle bundle) { Validate.notNull(bundle, "bundle"); return getDate(bundle, LAST_REFRESH_DATE_KEY); } public static void putLastRefreshDate(Bundle bundle, Date value) { Validate.notNull(bundle, "bundle"); Validate.notNull(value, "value"); putDate(bundle, LAST_REFRESH_DATE_KEY, value); } public static long getLastRefreshMilliseconds(Bundle bundle) { Validate.notNull(bundle, "bundle"); return bundle.getLong(LAST_REFRESH_DATE_KEY); } public static void putLastRefreshMilliseconds(Bundle bundle, long value) { Validate.notNull(bundle, "bundle"); bundle.putLong(LAST_REFRESH_DATE_KEY, value); } public static String getApplicationId(Bundle bundle) { Validate.notNull(bundle, "bundle"); return bundle.getString(APPLICATION_ID_KEY); } public static void putApplicationId(Bundle bundle, String value) { Validate.notNull(bundle, "bundle"); bundle.putString(APPLICATION_ID_KEY, value); } static Date getDate(Bundle bundle, String key) { if (bundle == null) { return null; } long n = bundle.getLong(key, INVALID_BUNDLE_MILLISECONDS); if (n == INVALID_BUNDLE_MILLISECONDS) { return null; } return new Date(n); } static void putDate(Bundle bundle, String key, Date date) { bundle.putLong(key, date.getTime()); } private void serializeKey(String key, Bundle bundle, SharedPreferences.Editor editor) throws JSONException { Object value = bundle.get(key); if (value == null) { // Cannot serialize null values. return; } String supportedType = null; JSONArray jsonArray = null; JSONObject json = new JSONObject(); if (value instanceof Byte) { supportedType = TYPE_BYTE; json.put(JSON_VALUE, ((Byte)value).intValue()); } else if (value instanceof Short) { supportedType = TYPE_SHORT; json.put(JSON_VALUE, ((Short)value).intValue()); } else if (value instanceof Integer) { supportedType = TYPE_INTEGER; json.put(JSON_VALUE, ((Integer)value).intValue()); } else if (value instanceof Long) { supportedType = TYPE_LONG; json.put(JSON_VALUE, ((Long)value).longValue()); } else if (value instanceof Float) { supportedType = TYPE_FLOAT; json.put(JSON_VALUE, ((Float)value).doubleValue()); } else if (value instanceof Double) { supportedType = TYPE_DOUBLE; json.put(JSON_VALUE, ((Double)value).doubleValue()); } else if (value instanceof Boolean) { supportedType = TYPE_BOOLEAN; json.put(JSON_VALUE, ((Boolean)value).booleanValue()); } else if (value instanceof Character) { supportedType = TYPE_CHAR; json.put(JSON_VALUE, value.toString()); } else if (value instanceof String) { supportedType = TYPE_STRING; json.put(JSON_VALUE, (String)value); } else if (value instanceof Enum<?>) { supportedType = TYPE_ENUM; json.put(JSON_VALUE, value.toString()); json.put(JSON_VALUE_ENUM_TYPE, value.getClass().getName()); } else { // Optimistically create a JSONArray. If not an array type, we can null // it out later jsonArray = new JSONArray(); if (value instanceof byte[]) { supportedType = TYPE_BYTE_ARRAY; for (byte v : (byte[])value) { jsonArray.put((int)v); } } else if (value instanceof short[]) { supportedType = TYPE_SHORT_ARRAY; for (short v : (short[])value) { jsonArray.put((int)v); } } else if (value instanceof int[]) { supportedType = TYPE_INTEGER_ARRAY; for (int v : (int[])value) { jsonArray.put(v); } } else if (value instanceof long[]) { supportedType = TYPE_LONG_ARRAY; for (long v : (long[])value) { jsonArray.put(v); } } else if (value instanceof float[]) { supportedType = TYPE_FLOAT_ARRAY; for (float v : (float[])value) { jsonArray.put((double)v); } } else if (value instanceof double[]) { supportedType = TYPE_DOUBLE_ARRAY; for (double v : (double[])value) { jsonArray.put(v); } } else if (value instanceof boolean[]) { supportedType = TYPE_BOOLEAN_ARRAY; for (boolean v : (boolean[])value) { jsonArray.put(v); } } else if (value instanceof char[]) { supportedType = TYPE_CHAR_ARRAY; for (char v : (char[])value) { jsonArray.put(String.valueOf(v)); } } else if (value instanceof List<?>) { supportedType = TYPE_STRING_LIST; @SuppressWarnings("unchecked") List<String> stringList = (List<String>)value; for (String v : stringList) { jsonArray.put((v == null) ? JSONObject.NULL : v); } } else { // Unsupported type. Clear out the array as a precaution even though // it is redundant with the null supportedType. jsonArray = null; } } if (supportedType != null) { json.put(JSON_VALUE_TYPE, supportedType); if (jsonArray != null) { // If we have an array, it has already been converted to JSON. So use // that instead. json.putOpt(JSON_VALUE, jsonArray); } String jsonString = json.toString(); editor.putString(key, jsonString); } } private void deserializeKey(String key, Bundle bundle) throws JSONException { String jsonString = cache.getString(key, "{}"); JSONObject json = new JSONObject(jsonString); String valueType = json.getString(JSON_VALUE_TYPE); if (valueType.equals(TYPE_BOOLEAN)) { bundle.putBoolean(key, json.getBoolean(JSON_VALUE)); } else if (valueType.equals(TYPE_BOOLEAN_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); boolean[] array = new boolean[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = jsonArray.getBoolean(i); } bundle.putBooleanArray(key, array); } else if (valueType.equals(TYPE_BYTE)) { bundle.putByte(key, (byte)json.getInt(JSON_VALUE)); } else if (valueType.equals(TYPE_BYTE_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); byte[] array = new byte[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = (byte)jsonArray.getInt(i); } bundle.putByteArray(key, array); } else if (valueType.equals(TYPE_SHORT)) { bundle.putShort(key, (short)json.getInt(JSON_VALUE)); } else if (valueType.equals(TYPE_SHORT_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); short[] array = new short[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = (short)jsonArray.getInt(i); } bundle.putShortArray(key, array); } else if (valueType.equals(TYPE_INTEGER)) { bundle.putInt(key, json.getInt(JSON_VALUE)); } else if (valueType.equals(TYPE_INTEGER_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); int[] array = new int[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = jsonArray.getInt(i); } bundle.putIntArray(key, array); } else if (valueType.equals(TYPE_LONG)) { bundle.putLong(key, json.getLong(JSON_VALUE)); } else if (valueType.equals(TYPE_LONG_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); long[] array = new long[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = jsonArray.getLong(i); } bundle.putLongArray(key, array); } else if (valueType.equals(TYPE_FLOAT)) { bundle.putFloat(key, (float)json.getDouble(JSON_VALUE)); } else if (valueType.equals(TYPE_FLOAT_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); float[] array = new float[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = (float)jsonArray.getDouble(i); } bundle.putFloatArray(key, array); } else if (valueType.equals(TYPE_DOUBLE)) { bundle.putDouble(key, json.getDouble(JSON_VALUE)); } else if (valueType.equals(TYPE_DOUBLE_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); double[] array = new double[jsonArray.length()]; for (int i = 0; i < array.length; i++) { array[i] = jsonArray.getDouble(i); } bundle.putDoubleArray(key, array); } else if (valueType.equals(TYPE_CHAR)) { String charString = json.getString(JSON_VALUE); if (charString != null && charString.length() == 1) { bundle.putChar(key, charString.charAt(0)); } } else if (valueType.equals(TYPE_CHAR_ARRAY)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); char[] array = new char[jsonArray.length()]; for (int i = 0; i < array.length; i++) { String charString = jsonArray.getString(i); if (charString != null && charString.length() == 1) { array[i] = charString.charAt(0); } } bundle.putCharArray(key, array); } else if (valueType.equals(TYPE_STRING)) { bundle.putString(key, json.getString(JSON_VALUE)); } else if (valueType.equals(TYPE_STRING_LIST)) { JSONArray jsonArray = json.getJSONArray(JSON_VALUE); int numStrings = jsonArray.length(); ArrayList<String> stringList = new ArrayList<String>(numStrings); for (int i = 0; i < numStrings; i++) { Object jsonStringValue = jsonArray.get(i); stringList.add( i, jsonStringValue == JSONObject.NULL ? null : (String)jsonStringValue); } bundle.putStringArrayList(key, stringList); } else if (valueType.equals(TYPE_ENUM)) { try { String enumType = json.getString(JSON_VALUE_ENUM_TYPE); @SuppressWarnings({ "unchecked", "rawtypes" }) Class<? extends Enum> enumClass = (Class<? extends Enum>) Class.forName(enumType); @SuppressWarnings("unchecked") Enum<?> enumValue = Enum.valueOf(enumClass, json.getString(JSON_VALUE)); bundle.putSerializable(key, enumValue); } catch (ClassNotFoundException e) { } catch (IllegalArgumentException e) { } } } }