content: /**
   * Copyright 2010-present Facebook.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  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.List;
  import java.util.Map;
  
  /*
   * <p>
   * An implementation of {@link TokenCachingStrategy TokenCachingStrategy} that uses Android SharedPreferences
   * to persist information.
   * </p>
   * <p>
   * The data to be cached is passed in via a Bundle. Only non-null key-value-pairs where
   * the value is one of the following types (or an array of the same) are persisted:
   * boolean, byte, int, long, float, double, char. In addition, String and List<String>
   * are also supported.
   * </p>
   */
  public class SharedPreferencesTokenCachingStrategy extends TokenCachingStrategy {
  
      private static final String DEFAULT_CACHE_KEY = "com.facebook.SharedPreferencesTokenCachingStrategy.DEFAULT_KEY";
      private static final String TAG = SharedPreferencesTokenCachingStrategy.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;
  
      /**
       * Creates a default {@link SharedPreferencesTokenCachingStrategy SharedPreferencesTokenCachingStrategy}
       * instance that provides access to a single set of token information.
       *
       * @param context
       *              The Context object to use to get the SharedPreferences object.
       *
       * @throws NullPointerException if the passed in Context is null
       */
      public SharedPreferencesTokenCachingStrategy(Context context) {
          this(context, null);
      }
  
      /**
       * Creates a {@link SharedPreferencesTokenCachingStrategy SharedPreferencesTokenCachingStrategy} instance
       * that is distinct for the passed in cacheKey.
       *
       * @param context
       *              The Context object to use to get the SharedPreferences object.
       *
       * @param cacheKey
       *              Identifies a distinct set of token information.
       *
       * @throws NullPointerException if the passed in Context is null
       */
      public SharedPreferencesTokenCachingStrategy(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);
      }
  
      /**
       * Returns a Bundle that contains the information stored in this cache
       *
       * @return A Bundle with the information contained in this cache
       */
      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;
      }
  
      /**
       * Persists all supported data types present in the passed in Bundle, to the
       * cache
       *
       * @param bundle
       *          The Bundle containing information to be cached
       */
      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;
              }
          }
  
          boolean successfulCommit = editor.commit();
          if (!successfulCommit) {
              Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "SharedPreferences.Editor.commit() was not successful");
          }
      }
  
      /**
       * Clears out all token information stored in this cache.
       */
      public void clear() {
          cache.edit().clear().commit();
      }
  
      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) {
              }
          }
      }
  }
  