gt:     protected void resetInternal(@NonNull Context context) {
context: /*
   * Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; either version 2
   * of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   * MA  02110-1301, USA.
   */
  package com.achep.base.content;
  
  import android.content.Context;
  import android.content.SharedPreferences;
  import android.content.res.Resources;
  import android.os.Handler;
  import android.os.Looper;
  import android.preference.Preference;
  import android.preference.PreferenceScreen;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import android.text.TextUtils;
  import android.util.Log;
  
  import com.achep.base.Device;
  import com.achep.base.interfaces.IBackupable;
  import com.achep.base.interfaces.IOnLowMemory;
  import com.achep.base.interfaces.ISubscriptable;
  import com.achep.base.tests.Check;
  import com.achep.base.utils.GzipUtils;
  
  import org.apache.commons.lang.builder.EqualsBuilder;
  import org.apache.commons.lang.builder.HashCodeBuilder;
  import org.json.JSONException;
  import org.json.JSONObject;
  
  import java.lang.ref.SoftReference;
  import java.lang.ref.WeakReference;
  import java.lang.reflect.Field;
  import java.lang.reflect.Method;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.Map;
  
  import timber.log.Timber;
  
  import static com.achep.base.Build.DEBUG;
  
  /**
   * Saves all the configurations for the app.
   *
   * @author Artem Chepurnoy
   * @since 21.01.14
   */
  @SuppressWarnings("ConstantConditions")
  public abstract class ConfigBase implements
          ISubscriptable<ConfigBase.OnConfigChangedListener>,
          IOnLowMemory, IBackupable {
  
      private static final String TAG = "Config";
  
      protected static final String PREFERENCES_FILE_NAME = "config";
  
      private final ArrayList<WeakReference<OnConfigChangedListener>> mListenersRefs = new ArrayList<>(6);
      private volatile SoftReference<Map<String, Option>> mMapRef = new SoftReference<>(null);
      private volatile Context mContext;
      private volatile Object mPreviousValue;
  
      // Threading
      protected final Handler mHandler = new Handler(Looper.getMainLooper());
  
      /**
       * Interface definition for a callback to be invoked
       * when a config is changed.
       */
      public interface OnConfigChangedListener {
  
          /**
           * Callback that the config has changed.
           *
           * @param config an instance of config
           * @param value  a new value of changed option
           */
          void onConfigChanged(
                  @NonNull ConfigBase config,
                  @NonNull String key,
                  @NonNull Object value);
  
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onLowMemory() {
          mMapRef.clear(); // it will be recreated in #getMap().
      }
  
      /**
       * Adds new {@link java.lang.ref.WeakReference weak} listener to the config. Make sure you call
       * {@link #unregisterListener(ConfigBase.OnConfigChangedListener)} later!
       *
       * @param listener a listener to register to config changes.
       * @see #unregisterListener(ConfigBase.OnConfigChangedListener)
       */
      @Override
      public final void registerListener(@NonNull OnConfigChangedListener listener) {
          // Make sure to register listener only once.
          for (WeakReference<OnConfigChangedListener> ref : mListenersRefs) {
              if (ref.get() == listener) {
                  Timber.tag(TAG).w("Tried to register already registered listener!");
                  return;
              }
          }
  
          mListenersRefs.add(new WeakReference<>(listener));
      }
  
      /**
       * Un-registers listener is there's one.
       *
       * @param listener a listener to unregister from config changes.
       * @see #registerListener(ConfigBase.OnConfigChangedListener)
       */
      @Override
      public final void unregisterListener(@NonNull OnConfigChangedListener listener) {
          for (WeakReference<OnConfigChangedListener> ref : mListenersRefs) {
              if (ref.get() == listener) {
                  mListenersRefs.remove(ref);
                  return;
              }
          }
  
          Timber.tag(TAG).w("Tried to unregister non-existent listener!");
      }
  
      /**
       * @return the {@link java.util.HashMap HashMap} with option's keys as the keys, and
       * its {@link Option data} as the values.
       * @see #onCreateMap(java.util.Map)
       */
      @NonNull
      public final Map<String, Option> getMap() {
          Map<String, Option> map = mMapRef.get();
          if (map == null) {
              map = new HashMap<>();
              onCreateMap(map);
              mMapRef = new SoftReference<>(map);
          }
          return map;
      }
  
      /**
       * @param key The unique key of the option.
       * @throws RuntimeException if failed to find the corresponding option.
       * @see #getMap()
       */
      @NonNull
      public final Option getOption(@NonNull String key) {
          Option option = getMap().get(key);
          if (option != null) return option;
  
          throw new RuntimeException("You have forgotten to put #"
                  + key + " to the hash map of config.");
      }
  
      /**
       * You may get a context from here only on
       * {@link ConfigBase.OnConfigChangedListener#onConfigChanged(ConfigBase, String, Object) config change}.
       */
      public Context getContext() {
          return mContext;
      }
  
      /**
       * You may get the previous value from here only on
       * {@link ConfigBase.OnConfigChangedListener#onConfigChanged(ConfigBase, String, Object) config change}.
       */
      @Nullable
      public Object getPreviousValue() {
          return mPreviousValue;
      }
  
      //-- INTERNAL METHODS -----------------------------------------------------
  
      /**
       * Gets an instance of the shared preferences of {@link #PREFERENCES_FILE_NAME}. By
       * default, the name is {@link #PREFERENCES_FILE_NAME}.
       */
      @NonNull
      protected SharedPreferences getSharedPreferences(@NonNull Context context) {
          return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE);
      }
  
      /**
       * Fills the {@link java.util.HashMap hash map} with config's options.
       *
       * @see #getMap()
       */
      protected abstract void onCreateMap(@NonNull Map<String, Option> map);
  
      protected abstract void onOptionChanged(@NonNull Option option, @NonNull String key);
  
      protected void writeFromMain(final @NonNull Context context,
                                   final @NonNull Option option, final @NonNull Object value,
                                   final @Nullable OnConfigChangedListener listenerToBeIgnored) {
          mHandler.post(new Runnable() {
  
              @Override
              public void run() {
                  write(context, option, value, listenerToBeIgnored);
              }
  
          });
      }
  
      protected void write(final @NonNull Context context,
                           final @NonNull Option option,
                           final @NonNull Object value,
                           final @Nullable OnConfigChangedListener listenerToBeIgnored) {
          Check.getInstance().isInMainThread();
  
          if (option.read(ConfigBase.this).equals(value)) return;
          String key = option.getKey(ConfigBase.this);
  
          if (DEBUG) Log.d(TAG, "Writing \"" + key + "=" + value + "\" to config.");
  
          // Read the current value from an option.
          mPreviousValue = option.read(this);
  
          // Set the current value to the field.
          try {
              Field field = getClass().getDeclaredField(option.fieldName);
              field.setAccessible(true);
              field.set(this, value);
          } catch (Exception e) {
              throw new IllegalStateException("");
          }
  
          // Set the current value to the preferences file.
          SharedPreferences.Editor editor = getSharedPreferences(context).edit();
          if (value instanceof Boolean) {
              editor.putBoolean(key, (Boolean) value);
          } else if (value instanceof Integer) {
              editor.putInt(key, (Integer) value);
          } else if (value instanceof Float) {
              editor.putFloat(key, (Float) value);
          } else if (value instanceof String) {
              editor.putString(key, (String) value);
          } else throw new IllegalArgumentException("Unknown option\'s type.");
          editor.apply();
  
          mContext = context;
          onOptionChanged(option, key);
          notifyConfigChanged(key, value, listenerToBeIgnored);
          mContext = null;
          mPreviousValue = null;
      }
  
      /**
       * @param key   the key of the option
       * @param value the new value
       * @see ConfigBase.OnConfigChangedListener#onConfigChanged(ConfigBase, String, Object)
       */
      private void notifyConfigChanged(@NonNull String key, @NonNull Object value,
                                       @Nullable OnConfigChangedListener listenerToBeIgnored) {
          Check.getInstance().isInMainThread();
  
          for (int i = mListenersRefs.size() - 1; i >= 0; i--) {
              WeakReference<OnConfigChangedListener> ref = mListenersRefs.get(i);
              OnConfigChangedListener l = ref.get();
  
              if (l == null) {
                  // There were no links to this listener except
                  // our class.
                  Log.w(TAG, "Deleting an addled listener..!");
                  mListenersRefs.remove(i);
              } else if (l != listenerToBeIgnored) {
                  l.onConfigChanged(this, key, value);
              }
          }
      }
  
      //-- BACKUP ---------------------------------------------------------------
  
      /**
       * Stores all the values to a JSON string and compresses it
       * using {@link GzipUtils Gzip}.
       *
       * @return the backup string or {@code null} if failed to generate the one.
       * @see #fromBackupText(Context, String)
       */
      @Override
      @Nullable
      public String toBackupText() {
          JSONObject json;
          try {
              json = new JSONObject();
              /*
              // TODO: Should I protect it somehow?
              json.put("__package__", "");
              json.put("__version__", "");
              */
              // Fill the json with key/value pairs
              for (Map.Entry<String, Option> entry : getMap().entrySet()) {
                  json.put(entry.getKey(), entry.getValue());
              }
          } catch (JSONException e) {
              Log.w(TAG, "Failed to generate JSON: " + e.getMessage());
              return null;
          }
  
          // We compress the result to protect it from noobs' changes
          // and to reduce its size. This is still easy to extract if
          // you know what to do.
          return GzipUtils.compress(json.toString());
      }
  
      /**
       * Loads all the settings from previously {@link #toBackupText() generated} backup string.
       * Technically this may broke current settings, so it's kinda dangerous.
       *
       * @return {@code true} if the config was successfully restored, {@code false} otherwise.
       * @see #toBackupText()
       */
      @Override
      public boolean fromBackupText(@NonNull Context context, @NonNull String input) {
          String json = GzipUtils.decompress(input);
          if (json == null) return false;
          String fallback = toBackupText(); // We can't risk
          return fallback != null && fromBackupText(context, json, fallback);
      }
  
      private boolean fromBackupText(@NonNull Context context,
                                     @NonNull String str, @NonNull String fallback) {
          try {
              JSONObject json = new JSONObject(str);
              Iterator<String> i = json.keys();
              while (i.hasNext()) {
                  String key = i.next();
                  Object value = json.get(key);
                  // Apply the value
                  Option option = getMap().get(key);
                  if (option != null) {
                      option.write(this, context, value, null);
                  } else {
                      Log.w(TAG, "Passed loading an unknown item[" + key + "] from plain text.");
                  }
              }
          } catch (Exception e) {
              // Try to fallback to original settings.
              if (!TextUtils.equals(str, fallback)) fromBackupText(context, fallback, fallback);
              // At this point current config may be partially corrupted and un-recoverable.
              return false;
          }
          return true;
      }
  
      //-- OTHER ----------------------------------------------------------------
  
      protected void initInternal(@NonNull Context context) {
          try {
              Resources res = context.getResources();
              SharedPreferences prefs = getSharedPreferences(context);
              for (Map.Entry<String, Option> entry : getMap().entrySet()) {
                  final String key = entry.getKey();
                  final Option option = entry.getValue();
  
                  // Get the current value.
                  Object value = option.getDefault(res);
                  if (boolean.class.isAssignableFrom(option.clazz)) {
                      value = prefs.getBoolean(key, (Boolean) value);
                  } else if (int.class.isAssignableFrom(option.clazz)) {
                      value = prefs.getInt(key, (Integer) value);
                  } else if (float.class.isAssignableFrom(option.clazz)) {
                      value = prefs.getFloat(key, (Float) value);
                  } else if (String.class.isAssignableFrom(option.clazz)) {
                      value = prefs.getString(key, (String) value);
                  } else throw new IllegalArgumentException("Unknown option\'s type.");
  
                  // Set the current value.
                  Field field = getClass().getDeclaredField(option.fieldName);
                  field.setAccessible(true);
                  field.set(this, value);
              }
          } catch (Exception e) {
              throw new RuntimeException();
          }
      }
  
  