T.serialize() = getSerializer().serialize(this)
+
+fun getSerializer(): Serializer {
+ if (serializer == null) {
+ throw ExceptionInInitializerError("serializer is null")
+ } else {
+ return serializer!!
+ }
+}
+/*********序列化*********/
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/provide.kt b/preferences/src/main/java/com/forjrking/preferences/provide/provide.kt
new file mode 100644
index 0000000..99d8a4c
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/provide.kt
@@ -0,0 +1,45 @@
+package com.forjrking.preferences.provide
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.forjrking.preferences.provide.sharedpreferenceimpl.MultiProcessSharedPreferences
+import com.forjrking.preferences.provide.sharedpreferenceimpl.SharedPreferencesHelper
+
+
+/**
+ * @description:
+ * @author: 岛主
+ * @date: 2020/7/21 11:21
+ * @version: 1.0.0
+ */
+
+/***
+ *生成支持多进程的mmkv
+ * @param name xml名称
+ * @param cryptKey 加密密钥 mmkv加密密钥
+ * @param isMMKV 是否使用mmkv
+ * @param isMultiProcess 是否使用多进程 建议mmkv搭配使用 sp性能很差
+ *
+ * 此方法不提供MMKV初始化需要自己操作配置
+ */
+@JvmOverloads
+fun Context.createSharedPreferences(
+ name: String? = null,
+ cryptKey: String? = null,
+ isMultiProcess: Boolean = false,
+ isMMKV: Boolean = false
+): SharedPreferences {
+ val xmlName = "${if (name.isNullOrEmpty()) packageName else name}_kv"
+ return if (isMMKV) {
+ val mode = if (isMultiProcess) com.tencent.mmkv.MMKV.MULTI_PROCESS_MODE
+ else com.tencent.mmkv.MMKV.SINGLE_PROCESS_MODE
+ com.tencent.mmkv.MMKV.mmkvWithID(xmlName, mode, cryptKey)
+ } else {
+ val mode = Context.MODE_PRIVATE
+ if (isMultiProcess) {
+ MultiProcessSharedPreferences.getSharedPreferences(this, xmlName, mode)
+ } else {
+ SharedPreferencesHelper.getSharedPreferences(this, xmlName, mode)
+ }
+ }
+}
\ No newline at end of file
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/FileUtils.java b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/FileUtils.java
new file mode 100644
index 0000000..80197e0
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/FileUtils.java
@@ -0,0 +1,70 @@
+package com.forjrking.preferences.provide.sharedpreferenceimpl;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+final class FileUtils {
+ private static Class> mClass;
+ private static Method mSetPermissionsMethod;
+
+ public static int S_IRWXU = 00700;
+ public static int S_IRUSR = 00400;
+ public static int S_IWUSR = 00200;
+
+ public static int S_IRWXG = 00070;
+ public static int S_IRGRP = 00040;
+ public static int S_IWGRP = 00020;
+
+ public static int S_IROTH = 00004;
+ public static int S_IWOTH = 00002;
+ public static int S_IXOTH = 00001;
+
+
+ public static boolean init() {
+ try {
+ mClass = Class.forName("android.os.FileUtils");
+ mSetPermissionsMethod = mClass.getMethod("setPermissions", new Class[]{String.class, int.class, int.class, int.class});
+ return true;
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ public static int setPermissions(String file, int mode, int uid, int gid) {
+ try {
+ return (Integer) mSetPermissionsMethod.invoke(null, new Object[]{file, mode, uid, gid});
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+
+ return -1;
+ }
+
+ /**
+ * DES: 把磁盘缓存强制刷入磁盘中 同步
+ * TIME: 2019/1/15 0015 上午 10:17
+ */
+ public static boolean sync(FileOutputStream stream) {
+ try {
+ if (stream != null) {
+ stream.getFD().sync();
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+}
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/MultiProcessSharedPreferences.java b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/MultiProcessSharedPreferences.java
new file mode 100644
index 0000000..b181a98
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/MultiProcessSharedPreferences.java
@@ -0,0 +1,712 @@
+package com.forjrking.preferences.provide.sharedpreferenceimpl;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.UriMatcher;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import com.forjrking.preferences.BuildConfig;
+
+import java.lang.ref.SoftReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 使用ContentProvider实现多进程SharedPreferences读写;
+ * 1、ContentProvider天生支持多进程访问;
+ * 2、使用内部私有BroadcastReceiver实现多进程OnSharedPreferenceChangeListener监听;
+ *
+ * 使用方法:AndroidManifest.xml中添加provider申明:
+ *
+ * <provider android:name="com.tencent.mm.sdk.patformtools.MultiProcessSharedPreferences"
+ * android:authorities="com.tencent.mm.sdk.patformtools.MultiProcessSharedPreferences"
+ * android:exported="false" />
+ * <!-- authorities属性里面最好使用包名做前缀,apk在安装时authorities同名的provider需要校验签名,否则无法安装;--!/>
+ *
+ *
+ * ContentProvider方式实现要注意:
+ * 1、当ContentProvider所在进程android.os.Process.killProcess(pid)时,会导致整个应用程序完全意外退出或者ContentProvider所在进程重启;
+ * 重启报错信息:Acquiring provider for user 0: existing object's process dead;
+ * 2、如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用,因此put值时默认返回false,get值时默认返回null;
+ *
+ * 其他方式实现SharedPreferences的问题:
+ * 使用FileLock和FileObserver也可以实现多进程SharedPreferences读写,但是会有兼容性问题:
+ * 1、某些设备上卸载程序时锁文件无法删除导致卸载残留,进而导致无法重新安装该程序(报INSTALL_FAILED_UID_CHANGED错误);
+ * 2、某些设备上FileLock会导致僵尸进程出现进而导致耗电;
+ * 3、僵尸进程出现后,正常进程的FileLock会一直阻塞等待僵尸进程中的FileLock释放,导致进程一直阻塞;
+ *
+ * @author seven456@gmail.com
+ * @version 1.0
+ * @since JDK1.6
+ */
+public class MultiProcessSharedPreferences extends ContentProvider implements SharedPreferences {
+ private static final String TAG = "MicroMsg.MultiProcessSharedPreferences";
+ public static final boolean DEBUG = BuildConfig.DEBUG;
+ private Context mContext;
+ private String mName;
+ private int mMode;
+ private boolean mIsSafeMode;
+ private List> mListeners;
+ private BroadcastReceiver mReceiver;
+
+ private static String AUTHORITY;
+ private static volatile Uri AUTHORITY_URI;
+ private UriMatcher mUriMatcher;
+ private static final String KEY = "value";
+ private static final String KEY_NAME = "name";
+ private static final String PATH_WILDCARD = "*/";
+ private static final String PATH_GET_ALL = "getAll";
+ private static final String PATH_GET_STRING = "getString";
+ private static final String PATH_GET_INT = "getInt";
+ private static final String PATH_GET_LONG = "getLong";
+ private static final String PATH_GET_FLOAT = "getFloat";
+ private static final String PATH_GET_BOOLEAN = "getBoolean";
+ private static final String PATH_CONTAINS = "contains";
+ private static final String PATH_APPLY = "apply";
+ private static final String PATH_COMMIT = "commit";
+ private static final String PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER =
+ "registerOnSharedPreferenceChangeListener";
+ private static final String PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER =
+ "unregisterOnSharedPreferenceChangeListener";
+ private static final int GET_ALL = 1;
+ private static final int GET_STRING = 2;
+ private static final int GET_INT = 3;
+ private static final int GET_LONG = 4;
+ private static final int GET_FLOAT = 5;
+ private static final int GET_BOOLEAN = 6;
+ private static final int CONTAINS = 7;
+ private static final int APPLY = 8;
+ private static final int COMMIT = 9;
+ private static final int REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 10;
+ private static final int UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 11;
+ private Map mListenersCount;
+
+ private static class ReflectionUtil {
+
+ public static ContentValues contentValuesNewInstance(HashMap values) {
+ try {
+ Constructor c =
+ ContentValues.class.getDeclaredConstructor(new Class[]{HashMap.class}); // hide
+ c.setAccessible(true);
+ return c.newInstance(values);
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Editor editorPutStringSet(Editor editor, String key, Set values) {
+ try {
+ Method method = editor.getClass().getDeclaredMethod(
+ "putStringSet", new Class[]{String.class, Set.class}); // Android 3.0
+ return (Editor) method.invoke(editor, key, values);
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void editorApply(Editor editor) {
+ try {
+ Method method = editor.getClass().getDeclaredMethod("apply"); // Android 2.3
+ method.invoke(editor);
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void checkInitAuthority(Context context) {
+ if (AUTHORITY_URI == null) {
+ String AUTHORITY = null;
+ Uri AUTHORITY_URI = MultiProcessSharedPreferences.AUTHORITY_URI;
+ synchronized (MultiProcessSharedPreferences.this) {
+ if (AUTHORITY_URI == null) {
+ AUTHORITY = queryAuthority(context);
+ AUTHORITY_URI = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + AUTHORITY);
+ if (DEBUG) {
+// Log.d(TAG, "checkInitAuthority.AUTHORITY = " + AUTHORITY);
+ }
+ }
+ if (AUTHORITY == null) {
+ throw new IllegalArgumentException("'AUTHORITY' initialize failed.");
+ }
+ }
+ MultiProcessSharedPreferences.AUTHORITY = AUTHORITY;
+ MultiProcessSharedPreferences.AUTHORITY_URI = AUTHORITY_URI;
+ }
+ }
+
+ private static String queryAuthority(Context context) {
+ PackageInfo packageInfos = null;
+ try {
+ PackageManager mgr = context.getPackageManager();
+ if (mgr != null) {
+ packageInfos = mgr.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS);
+ }
+ } catch (NameNotFoundException e) {
+ if (DEBUG) {
+ // Log.printErrStackTrace(TAG, e, "");
+ }
+ }
+ if (packageInfos != null && packageInfos.providers != null) {
+ for (ProviderInfo providerInfo : packageInfos.providers) {
+ if (providerInfo.name.equals(MultiProcessSharedPreferences.class.getName())) {
+ return providerInfo.authority;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * mode不使用{@link Context#MODE_MULTI_PROCESS}特可以支持多进程了;
+ *
+ * @param mode
+ * @see Context#MODE_PRIVATE
+ * @see Context#MODE_WORLD_READABLE
+ * @see Context#MODE_WORLD_WRITEABLE
+ */
+ public static SharedPreferences getSharedPreferences(Context context, String name, int mode) {
+ return new MultiProcessSharedPreferences(context, name, mode);
+ }
+
+ public MultiProcessSharedPreferences() {
+ }
+
+ private MultiProcessSharedPreferences(Context context, String name, int mode) {
+ mContext = context;
+ mName = name;
+ mMode = mode;
+ PackageManager mgr = context.getPackageManager();
+ if (mgr != null) {
+ mIsSafeMode = mgr.isSafeMode(); // 如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map getAll() {
+ return (Map) getValue(PATH_GET_ALL, null, null);
+ }
+
+ @Override
+ public String getString(String key, String defValue) {
+ String v = (String) getValue(PATH_GET_STRING, key, defValue);
+ return v != null ? v : defValue;
+ }
+
+ // @Override // Android 3.0
+ public Set getStringSet(String key, Set defValues) {
+ synchronized (this) {
+ @SuppressWarnings("unchecked")
+ Set v = (Set) getValue(PATH_GET_STRING, key, defValues);
+ return v != null ? v : defValues;
+ }
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ Integer v = (Integer) getValue(PATH_GET_INT, key, defValue);
+ return v != null ? v : defValue;
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ Long v = (Long) getValue(PATH_GET_LONG, key, defValue);
+ return v != null ? v : defValue;
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ Float v = (Float) getValue(PATH_GET_FLOAT, key, defValue);
+ return v != null ? v : defValue;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ Boolean v = (Boolean) getValue(PATH_GET_BOOLEAN, key, defValue);
+ return v != null ? v : defValue;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ Boolean v = (Boolean) getValue(PATH_CONTAINS, key, null);
+ return v != null ? v : false;
+ }
+
+ @Override
+ public Editor edit() {
+ return new EditorImpl();
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized (this) {
+ if (mListeners == null) {
+ mListeners = new ArrayList>();
+ }
+ Boolean result = (Boolean) getValue(PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false);
+ if (result != null && result) {
+ mListeners.add(new SoftReference(listener));
+ if (mReceiver == null) {
+ mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String name = intent.getStringExtra(KEY_NAME);
+ @SuppressWarnings("unchecked")
+ List keysModified = (List) intent.getSerializableExtra(KEY);
+ if (mName.equals(name) && keysModified != null) {
+ List arrayListeners;
+ synchronized (MultiProcessSharedPreferences.this) {
+ arrayListeners = mListeners;
+ }
+ List> listeners =
+ new ArrayList>(arrayListeners);
+ for (int i = keysModified.size() - 1; i >= 0; i--) {
+ final String key = keysModified.get(i);
+ for (SoftReference srlistener : listeners) {
+ OnSharedPreferenceChangeListener listener = srlistener.get();
+ if (listener != null) {
+ listener.onSharedPreferenceChanged(MultiProcessSharedPreferences.this, key);
+ }
+ }
+ }
+ }
+ }
+ };
+ mContext.registerReceiver(mReceiver, new IntentFilter(makeAction(mName)));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized (this) {
+ getValue(PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false);
+ if (mListeners != null) {
+ List> removing = new ArrayList<>();
+ for (SoftReference srlistener : mListeners) {
+ OnSharedPreferenceChangeListener listenerFromSR = srlistener.get();
+ if (listenerFromSR != null && listenerFromSR.equals(listener)) {
+ removing.add(srlistener);
+ }
+ }
+ for (SoftReference srlistener : removing) {
+ mListeners.remove(srlistener);
+ }
+ if (mListeners.isEmpty() && mReceiver != null) {
+ mContext.unregisterReceiver(mReceiver);
+ mReceiver = null;
+ mListeners = null;
+ }
+ }
+ }
+ }
+
+ public final class EditorImpl implements Editor {
+ private final Map mModified = new HashMap();
+ private boolean mClear = false;
+
+ @Override
+ public Editor putString(String key, String value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ // @Override // Android 3.0
+ public Editor putStringSet(String key, Set values) {
+ synchronized (this) {
+ mModified.put(key, (values == null) ? null : new HashSet(values));
+ return this;
+ }
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ @Override
+ public Editor remove(String key) {
+ synchronized (this) {
+ mModified.put(key, null);
+ return this;
+ }
+ }
+
+ @Override
+ public Editor clear() {
+ synchronized (this) {
+ mClear = true;
+ return this;
+ }
+ }
+
+ @Override
+ public void apply() {
+ setValue(PATH_APPLY);
+ }
+
+ @Override
+ public boolean commit() {
+ return setValue(PATH_COMMIT);
+ }
+
+ private boolean setValue(String pathSegment) {
+ if (mIsSafeMode) { // 如果设备处在“安全模式”,返回false;
+ return false;
+ } else {
+ synchronized (MultiProcessSharedPreferences.this) {
+ checkInitAuthority(mContext);
+ String[] selectionArgs = new String[]{String.valueOf(mMode), String.valueOf(mClear)};
+ synchronized (this) {
+ Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment);
+ ContentValues values =
+ ReflectionUtil.contentValuesNewInstance((HashMap) mModified);
+ return mContext.getContentResolver().update(uri, values, null, selectionArgs) > 0;
+ }
+ }
+ }
+ }
+ }
+
+ private Object getValue(String pathSegment, String key, Object defValue) {
+ if (mIsSafeMode) { // 如果设备处在“安全模式”,返回null;
+ return null;
+ } else {
+ checkInitAuthority(mContext);
+ Object v = null;
+ Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment);
+ String[] selectionArgs =
+ new String[]{String.valueOf(mMode), key, defValue == null ? null : String.valueOf(defValue)};
+ Cursor cursor = mContext.getContentResolver().query(uri, null, null, selectionArgs, null);
+ if (cursor != null) {
+ try {
+ Bundle bundle = cursor.getExtras();
+ if (bundle != null) {
+ v = bundle.get(KEY);
+ bundle.clear();
+ }
+ } catch (Exception e) {
+ }
+ cursor.close();
+ }
+ return v != null ? v : defValue;
+ }
+ }
+
+ private String makeAction(String name) {
+ return String.format("%1$s_%2$s", MultiProcessSharedPreferences.class.getName(), name);
+ }
+
+ @Override
+ public boolean onCreate() {
+ checkInitAuthority(getContext());
+ mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_ALL, GET_ALL);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_STRING, GET_STRING);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_INT, GET_INT);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_LONG, GET_LONG);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_FLOAT, GET_FLOAT);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_BOOLEAN, GET_BOOLEAN);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_CONTAINS, CONTAINS);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_APPLY, APPLY);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_COMMIT, COMMIT);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
+ REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER);
+ mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
+ UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER);
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ String name = uri.getPathSegments().get(0);
+ int mode = Integer.parseInt(selectionArgs[0]);
+ String key = selectionArgs[1];
+ String defValue = selectionArgs[2];
+ Bundle bundle = new Bundle();
+ switch (mUriMatcher.match(uri)) {
+ case GET_ALL:
+ bundle.putSerializable(KEY,
+ (HashMap) getContext().getSharedPreferences(name, mode).getAll());
+ break;
+ case GET_STRING:
+ bundle.putString(KEY, getContext().getSharedPreferences(name, mode).getString(key, defValue));
+ break;
+ case GET_INT:
+ bundle.putInt(KEY,
+ getContext().getSharedPreferences(name, mode).getInt(key, Integer.parseInt(defValue)));
+ break;
+ case GET_LONG:
+ bundle.putLong(KEY,
+ getContext().getSharedPreferences(name, mode).getLong(key, Long.parseLong(defValue)));
+ break;
+ case GET_FLOAT:
+ bundle.putFloat(
+ KEY, getContext().getSharedPreferences(name, mode).getFloat(key, Float.parseFloat(defValue)));
+ break;
+ case GET_BOOLEAN:
+ bundle.putBoolean(
+ KEY, getContext().getSharedPreferences(name, mode).getBoolean(key, Boolean.parseBoolean(defValue)));
+ break;
+ case CONTAINS:
+ bundle.putBoolean(KEY, getContext().getSharedPreferences(name, mode).contains(key));
+ break;
+ case REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER: {
+ checkInitListenersCount();
+ Integer countInteger = mListenersCount.get(name);
+ int count = (countInteger == null ? 0 : countInteger) + 1;
+ mListenersCount.put(name, count);
+ countInteger = mListenersCount.get(name);
+ bundle.putBoolean(KEY, count == (countInteger == null ? 0 : countInteger));
+ }
+ break;
+ case UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER: {
+ checkInitListenersCount();
+ Integer countInteger = mListenersCount.get(name);
+ int count = (countInteger == null ? 0 : countInteger) - 1;
+ if (count <= 0) {
+ mListenersCount.remove(name);
+ bundle.putBoolean(KEY, !mListenersCount.containsKey(name));
+ } else {
+ mListenersCount.put(name, count);
+ countInteger = mListenersCount.get(name);
+ bundle.putBoolean(KEY, count == (countInteger == null ? 0 : countInteger));
+ }
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("This is Unknown Uri:" + uri);
+ }
+ return new BundleCursor(bundle);
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ throw new UnsupportedOperationException("No external call");
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException("No external insert");
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("No external delete");
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ int result = 0;
+ String name = uri.getPathSegments().get(0);
+ int mode = Integer.parseInt(selectionArgs[0]);
+ SharedPreferences preferences = getContext().getSharedPreferences(name, mode);
+ int match = mUriMatcher.match(uri);
+ switch (match) {
+ case APPLY:
+ case COMMIT:
+ boolean hasListeners =
+ mListenersCount != null && mListenersCount.get(name) != null && mListenersCount.get(name) > 0;
+ ArrayList keysModified = null;
+ Map map = new HashMap();
+ if (hasListeners) {
+ keysModified = new ArrayList();
+ map = (Map) preferences.getAll();
+ }
+ Editor editor = preferences.edit();
+ boolean clear = Boolean.parseBoolean(selectionArgs[1]);
+ if (clear) {
+ if (hasListeners && map != null && !map.isEmpty()) {
+ for (Map.Entry entry : map.entrySet()) {
+ keysModified.add(entry.getKey());
+ }
+ }
+ editor.clear();
+ }
+ for (Map.Entry entry : values.valueSet()) {
+ String k = entry.getKey();
+ Object v = entry.getValue();
+ // Android 5.L_preview : "this" is the magic value for a removal mutation. In addition,
+ // setting a value to "null" for a given key is specified to be
+ // equivalent to calling remove on that key.
+ if (v instanceof EditorImpl || v == null) {
+ editor.remove(k);
+ if (hasListeners && map != null && map.containsKey(k)) {
+ keysModified.add(k);
+ }
+ } else {
+ if (hasListeners && map != null
+ && (!map.containsKey(k) || (map.containsKey(k) && !v.equals(map.get(k))))) {
+ keysModified.add(k);
+ }
+ }
+
+ if (v instanceof String) {
+ editor.putString(k, (String) v);
+ } else if (v instanceof Set) {
+ ReflectionUtil.editorPutStringSet(editor, k,
+ (Set) v); // Android 3.0
+ } else if (v instanceof Integer) {
+ editor.putInt(k, (Integer) v);
+ } else if (v instanceof Long) {
+ editor.putLong(k, (Long) v);
+ } else if (v instanceof Float) {
+ editor.putFloat(k, (Float) v);
+ } else if (v instanceof Boolean) {
+ editor.putBoolean(k, (Boolean) v);
+ }
+ }
+ if (hasListeners && keysModified.isEmpty()) {
+ result = 1;
+ } else {
+ switch (match) {
+ case APPLY:
+ ReflectionUtil.editorApply(editor); // Android 2.3
+ result = 1;
+ // Okay to notify the listeners before it's hit disk
+ // because the listeners should always get the same
+ // SharedPreferences instance back, which has the
+ // changes reflected in memory.
+ notifyListeners(name, keysModified);
+ break;
+ case COMMIT:
+ if (editor.commit()) {
+ result = 1;
+ notifyListeners(name, keysModified);
+ }
+ break;
+ }
+ }
+ values.clear();
+ break;
+ default:
+ throw new IllegalArgumentException("This is Unknown Uri:" + uri);
+ }
+ return result;
+ }
+
+ @Override
+ public void onLowMemory() {
+ if (mListenersCount != null) {
+ mListenersCount.clear();
+ }
+ super.onLowMemory();
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ if (mListenersCount != null) {
+ mListenersCount.clear();
+ }
+ super.onTrimMemory(level);
+ }
+
+ private void checkInitListenersCount() {
+ if (mListenersCount == null) {
+ mListenersCount = new HashMap();
+ }
+ }
+
+ private void notifyListeners(String name, ArrayList keysModified) {
+ if (keysModified != null && !keysModified.isEmpty()) {
+ Intent intent = new Intent();
+ intent.setAction(makeAction(name));
+ intent.setPackage(getContext().getPackageName());
+ intent.putExtra(KEY_NAME, name);
+ intent.putExtra(KEY, keysModified);
+ getContext().sendBroadcast(intent);
+ }
+ }
+
+ private static final class BundleCursor extends MatrixCursor {
+ private Bundle mBundle;
+
+ public BundleCursor(Bundle extras) {
+ super(new String[]{}, 0);
+ mBundle = extras;
+ }
+
+ @Override
+ public Bundle getExtras() {
+ return mBundle;
+ }
+
+ @Override
+ public Bundle respond(Bundle extras) {
+ mBundle = extras;
+ return mBundle;
+ }
+ }
+}
\ No newline at end of file
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/QueuedWork.java b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/QueuedWork.java
new file mode 100644
index 0000000..c6b1f4d
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/QueuedWork.java
@@ -0,0 +1,124 @@
+package com.forjrking.preferences.provide.sharedpreferenceimpl;
+
+import android.os.Build;
+import android.os.Handler;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+
+
+final class QueuedWork {
+
+ // The set of Runnables that will finish or wait on any async
+ // activities started by the application.
+ private static final ConcurrentLinkedQueue sPendingWorkFinishers =
+ new ConcurrentLinkedQueue();
+
+ private static volatile boolean mCustomWaitToFinish;
+
+ private static Class> mClass;
+ private static Method mAddMethod;
+ private static Method mRemoveMethod;
+ private static ExecutorService mExecutorService;
+ private static Handler mHandler;
+
+ public static boolean init() {
+ if (Build.VERSION.SDK_INT >= 28) {
+ //android p 以后禁止反射 QueuedWork.getHandler 接口,所以直接使用系统的sp实现
+ return false;
+ }
+
+ try {
+ mClass = Class.forName("android.app.QueuedWork");
+ if (Build.VERSION.SDK_INT >= 26) {
+ try {
+ mAddMethod = mClass.getMethod("addFinisher", Runnable.class);
+ mRemoveMethod = mClass.getMethod("removeFinisher", Runnable.class);
+ Method method = mClass.getDeclaredMethod("getHandler", new Class[]{});
+ method.setAccessible(true);
+ mHandler = (Handler) method.invoke(null, new Object[]{});
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (mAddMethod == null || mRemoveMethod == null || mHandler == null) {
+ mAddMethod = mClass.getMethod("add", Runnable.class);
+ mRemoveMethod = mClass.getMethod("remove", Runnable.class);
+
+ Method method = mClass.getMethod("singleThreadExecutor", new Class[]{});
+ mExecutorService = (ExecutorService) method.invoke(null, new Object[]{});
+ }
+ return true;
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ private static void invokeClassMethod(Method method, Object arg) {
+ try {
+ method.invoke(null, arg);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void add(Runnable finisher) {
+ if (mCustomWaitToFinish) {
+ sPendingWorkFinishers.add(finisher);
+ } else {
+ invokeClassMethod(mAddMethod, finisher);
+ }
+ }
+
+ public static void remove(Runnable finisher) {
+ boolean success = mCustomWaitToFinish && sPendingWorkFinishers.remove(finisher);
+ if (!success) {
+ invokeClassMethod(mRemoveMethod, finisher);
+ }
+ }
+
+ public static void postRunnable(Runnable runnable) {
+ if (mHandler != null) {
+ mHandler.post(runnable);
+ }
+ }
+
+ public static ExecutorService singleThreadExecutor() {
+ return mExecutorService;
+ }
+
+ public static void setCustomWaitToFinish(boolean enable) {
+ if (mCustomWaitToFinish != enable) {
+ mCustomWaitToFinish = enable;
+ if (!enable) {
+ waitToFinish();
+ }
+ }
+ }
+
+ public static void waitToFinish() {
+ Runnable toFinish;
+ while ((toFinish = sPendingWorkFinishers.poll()) != null) {
+ toFinish.run();
+ }
+ }
+}
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/SharedPreferencesHelper.java b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/SharedPreferencesHelper.java
new file mode 100644
index 0000000..8526c71
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/SharedPreferencesHelper.java
@@ -0,0 +1,143 @@
+package com.forjrking.preferences.provide.sharedpreferenceimpl;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * http://gityuan.com/2017/06/18/SharedPreferences/ 全面剖析
+ * https://www.jianshu.com/p/3f64caa567e5 8.0以上已经有优化不需要这样操作了
+ * DES: sp的hook代理
+ * CHANGED: 岛主
+ * TIME: 2019/1/15 0015 上午 10:16
+ */
+public class SharedPreferencesHelper {
+ private static boolean mCanUseCustomSp = true;
+ private static boolean mHasCheck = false;
+
+ private static Method mGetSharedPrefsFileMethod;
+
+ private static volatile ExecutorService sCachedThreadPool;
+
+ public static synchronized boolean canUseCustomSp() {
+ if (!mHasCheck) {
+ mHasCheck = true;
+ if (!QueuedWork.init() || !FileUtils.init() || !XmlUtils.init()) {
+ mCanUseCustomSp = false;
+ }
+ }
+
+ return mCanUseCustomSp;
+ }
+
+ private static File getSharedPrefsFile(Context context, String name) {
+ if (mGetSharedPrefsFileMethod == null) {
+ try {
+ mGetSharedPrefsFileMethod = context.getClass().getMethod("getSharedPrefsFile", String.class);
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ }
+ }
+
+ File prefsFile = null;
+ if (mGetSharedPrefsFileMethod != null) {
+ try {
+ prefsFile = (File) mGetSharedPrefsFileMethod.invoke(context, name);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ return prefsFile;
+ }
+
+ /**
+ * 暂时只有这里需要用到cachedThreadPool。以后如果有更多业务需要用了再考虑提供统一接口。
+ * 使用cachedThreadPool是为了保证任务总是立即调度而不需要等待,并减少碎片化任务频繁创建线程的耗时
+ */
+ static void execute(Runnable task) {
+ if (sCachedThreadPool == null) {
+ synchronized (SharedPreferencesHelper.class) {
+ if (sCachedThreadPool == null) {
+ sCachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
+ 30L, TimeUnit.SECONDS,
+ new SynchronousQueue(), new SPThreadFactory());
+ }
+ }
+ }
+ sCachedThreadPool.execute(task);
+ }
+
+ private static class SPThreadFactory implements ThreadFactory {
+ private static final AtomicInteger poolNumber = new AtomicInteger(1);
+ private final ThreadGroup group;
+ private final AtomicInteger threadNumber = new AtomicInteger(1);
+ private final String namePrefix;
+
+ SPThreadFactory() {
+ SecurityManager s = System.getSecurityManager();
+ group = (s != null) ? s.getThreadGroup() :
+ Thread.currentThread().getThreadGroup();
+ namePrefix = "sp-" + poolNumber.getAndIncrement() + "-thread-";
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(group, r,
+ namePrefix + threadNumber.getAndIncrement(),
+ 0);
+ if (t.isDaemon())
+ t.setDaemon(false);
+ if (t.getPriority() != Thread.NORM_PRIORITY)
+ t.setPriority(Thread.NORM_PRIORITY);
+ return t;
+ }
+ }
+
+ /**
+ * DES: 静态保存的内存值
+ * TIME: 2019/1/15 0015 上午 9:53
+ */
+ private static final HashMap sSharedPrefs = new HashMap();
+
+ /**
+ * DES: 返回hook的SharedPreferences实例
+ * TIME: 2019/1/15 0015 上午 9:54
+ */
+ public static SharedPreferences getSharedPreferences(Context context, String name, int mode) {
+ if (!SharedPreferencesHelper.canUseCustomSp()) {
+ return context.getSharedPreferences(name, mode);
+ }
+ SharedPreferencesImpl sp;
+
+ synchronized (sSharedPrefs) {
+ sp = sSharedPrefs.get(name);
+ if (sp == null) {
+ File prefsFile = SharedPreferencesHelper.getSharedPrefsFile(context, name);
+ sp = new SharedPreferencesImpl(prefsFile, mode);
+ sSharedPrefs.put(name, sp);
+ return sp;
+ }
+ }
+
+ if ((mode & Context.MODE_MULTI_PROCESS) != 0) {
+ // If somebody else (some other process) changed the prefs
+ // file behind our back, we reload it. This has been the
+ // historical (if undocumented) behavior.
+ sp.startReloadIfChangedUnexpectedly();
+ }
+
+ return sp;
+ }
+}
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/SharedPreferencesImpl.java b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/SharedPreferencesImpl.java
new file mode 100644
index 0000000..07343c4
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/SharedPreferencesImpl.java
@@ -0,0 +1,677 @@
+package com.forjrking.preferences.provide.sharedpreferenceimpl;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * 岛主
+ * DES: SharedPreferences实现类,内部优化了 异步线程的优先级和apply()方法落盘的异步机制
+ * TIME: 2019/1/15 0015 上午 9:58
+ */
+final class SharedPreferencesImpl implements SharedPreferences {
+ private static final String TAG = "SharedPreferencesImpl";
+ private static final boolean DEBUG = false;
+
+ // Lock ordering rules:
+ // - acquire SharedPreferencesImpl.this before EditorImpl.this
+ // - acquire mWritingToDiskLock before EditorImpl.this
+
+ private final File mFile;
+ private final File mBackupFile;
+ private final int mMode;
+
+ private Map mMap; // guarded by 'this'
+ private int mDiskWritesInFlight = 0; // guarded by 'this'
+ private boolean mLoaded = false; // guarded by 'this'
+ private long mStatTimestamp; // guarded by 'this'
+ private long mStatSize; // guarded by 'this'
+ private boolean mChangesMade; // guarded by 'this'
+
+ private final Object mWritingToDiskLock = new Object();
+ private static final Object mContent = new Object();
+ private final WeakHashMap mListeners =
+ new WeakHashMap();
+
+ private Handler mMainHandler = new Handler(Looper.getMainLooper());
+
+ private volatile int mLoadTid = -1;
+ private volatile int mLoadPriority = Process.THREAD_PRIORITY_BACKGROUND;
+
+ public SharedPreferencesImpl(File file, int mode) {
+ mFile = file;
+ mBackupFile = makeBackupFile(file);
+ mMode = mode;
+ mLoaded = false;
+ mMap = null;
+
+ startLoadFromDisk();
+ }
+
+ private void startLoadFromDisk() {
+ synchronized (this) {
+ mLoaded = false;
+ }
+ SharedPreferencesHelper.execute(new Runnable() {
+ @Override
+ public void run() {
+ mLoadTid = Process.myTid();
+ Process.setThreadPriority(mLoadPriority);
+ loadFromDisk();
+ synchronized (SharedPreferencesImpl.this) {
+ mLoadTid = -1;
+ }
+ mLoadPriority = Process.THREAD_PRIORITY_BACKGROUND;
+ }
+ });
+ }
+
+ private void loadFromDisk() {
+ synchronized (SharedPreferencesImpl.this) {
+ if (mLoaded) {
+ return;
+ }
+ if (mBackupFile.exists()) {
+ mFile.delete();
+ mBackupFile.renameTo(mFile);
+ }
+ }
+
+ // Debugging
+ if (mFile.exists() && !mFile.canRead()) {
+ Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
+ }
+
+ long ts = mFile.lastModified();
+ long size = mFile.length();
+
+ Map map = null;
+ if (mFile.canRead()) {
+ BufferedInputStream str = null;
+ try {
+ str = new BufferedInputStream(
+ new FileInputStream(mFile), 16 * 1024);
+ map = XmlUtils.readMapXml(str);
+ } catch (Exception e) {
+ Log.w(TAG, "getSharedPreferences", e);
+ } finally {
+ if (str != null) {
+ try {
+ str.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+
+ synchronized (SharedPreferencesImpl.this) {
+ mLoaded = true;
+ mChangesMade = false;
+ if (map != null) {
+ mMap = map;
+ mStatTimestamp = ts;
+ mStatSize = size;
+ } else {
+ mMap = new HashMap();
+ }
+ notifyAll();
+ }
+ }
+
+ static File makeBackupFile(File prefsFile) {
+ return new File(prefsFile.getPath() + ".bak");
+ }
+
+ public void startReloadIfChangedUnexpectedly() {
+ synchronized (this) {
+ // TODO: wait for any pending writes to disk?
+ if (!hasFileChangedUnexpectedly()) {
+ return;
+ }
+ startLoadFromDisk();
+ }
+ }
+
+ // Has the file changed out from under us? i.e. writes that
+ // we didn't instigate.
+ private boolean hasFileChangedUnexpectedly() {
+ synchronized (this) {
+ if (mDiskWritesInFlight > 0) {
+ // If we know we caused it, it's not unexpected.
+ if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
+ return false;
+ }
+ }
+
+ if (!mFile.exists()) {
+ return true;
+ }
+
+ long ts = mFile.lastModified();
+ long size = mFile.length();
+
+ /*
+ * Metadata operations don't usually count as a block guard
+ * violation, but we explicitly want this one.
+ */
+ //BlockGuard.onReadFromDisk();
+
+ synchronized (this) {
+ return mStatTimestamp != ts || mStatSize != size;
+ }
+ }
+
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized (this) {
+ mListeners.put(listener, mContent);
+ }
+ }
+
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+ synchronized (this) {
+ mListeners.remove(listener);
+ }
+ }
+
+ private void awaitLoadedLocked() {
+ //if (!mLoaded) {
+ // // Raise an explicit StrictMode onReadFromDisk for this
+ // // thread, since the real read will be in a different
+ // // thread and otherwise ignored by StrictMode.
+ // BlockGuard.onReadFromDisk();
+ //}
+ while (!mLoaded) {
+ adjustLoadPriority();
+ try {
+ wait();
+ } catch (InterruptedException unused) {
+ }
+ }
+ }
+
+ /**
+ * 调整加载线程优先级,避免调用线程等待太长时间
+ */
+ private final void adjustLoadPriority() {
+ int priority = Process.getThreadPriority(Process.myTid());
+ if (priority < mLoadPriority) {
+ mLoadPriority = priority;
+ if (mLoadTid != -1) {
+ Process.setThreadPriority(mLoadTid, mLoadPriority);
+ }
+ }
+ }
+
+ public Map getAll() {
+ synchronized (this) {
+ awaitLoadedLocked();
+ //noinspection unchecked
+ return new HashMap(mMap);
+ }
+ }
+
+ public String getString(String key, String defValue) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ String v = (String) mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public Set getStringSet(String key, Set defValues) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ Set v = (Set) mMap.get(key);
+ return v != null ? v : defValues;
+ }
+ }
+
+ public int getInt(String key, int defValue) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ Integer v = (Integer) mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public long getLong(String key, long defValue) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ Long v = (Long) mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public float getFloat(String key, float defValue) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ Float v = (Float) mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ Boolean v = (Boolean) mMap.get(key);
+ return v != null ? v : defValue;
+ }
+ }
+
+ public boolean contains(String key) {
+ synchronized (this) {
+ awaitLoadedLocked();
+ return mMap.containsKey(key);
+ }
+ }
+
+ public Editor edit() {
+ // TODO: remove the need to call awaitLoadedLocked() when
+ // requesting an editor. will require some work on the
+ // Editor, but then we should be able to do:
+ //
+ // context.getSharedPreferences(..).edit().putString(..).apply()
+ //
+ // ... all without blocking.
+ synchronized (this) {
+ awaitLoadedLocked();
+ }
+
+ return new EditorImpl();
+ }
+
+ // return value from editorimpl#committomemory()
+ private static class MemoryCommitResult {
+ public List keysModified; // may be null
+ public Set listeners; // may be null
+ public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
+ public volatile boolean writeToDiskResult = false;
+
+ public void setDiskWriteResult(boolean result) {
+ writeToDiskResult = result;
+ writtenToDiskLatch.countDown();
+ }
+ }
+
+ public final class EditorImpl implements Editor {
+ private final Map mModified = new HashMap();
+ private boolean mClear = false;
+
+ public Editor putString(String key, String value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor putStringSet(String key, Set values) {
+ synchronized (this) {
+ mModified.put(key,
+ (values == null) ? null : new HashSet(values));
+ return this;
+ }
+ }
+
+ public Editor putInt(String key, int value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor putLong(String key, long value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor putFloat(String key, float value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor putBoolean(String key, boolean value) {
+ synchronized (this) {
+ mModified.put(key, value);
+ return this;
+ }
+ }
+
+ public Editor remove(String key) {
+ synchronized (this) {
+ mModified.put(key, this);
+ return this;
+ }
+ }
+
+ public Editor clear() {
+ synchronized (this) {
+ mClear = true;
+ return this;
+ }
+ }
+
+ public void apply() {
+ final MemoryCommitResult mcr = commitToMemory();
+
+ boolean hasDiskWritesInFlight = false;
+ synchronized (SharedPreferencesImpl.this) {
+ hasDiskWritesInFlight = mDiskWritesInFlight > 0;
+ }
+
+ if (!hasDiskWritesInFlight) {
+ final Runnable awaitCommit = new Runnable() {
+ public void run() {
+ try {
+ mcr.writtenToDiskLatch.await();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ };
+
+ QueuedWork.add(awaitCommit);
+
+ Runnable postWriteRunnable = new Runnable() {
+ public void run() {
+ awaitCommit.run();
+
+ QueuedWork.remove(awaitCommit);
+ }
+ };
+
+ SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
+ }
+
+ // Okay to notify the listeners before it's hit disk
+ // because the listeners should always get the same
+ // SharedPreferences instance back, which has the
+ // changes reflected in memory.
+ notifyListeners(mcr);
+ }
+
+ // Returns true if any changes were made
+ private MemoryCommitResult commitToMemory() {
+ MemoryCommitResult mcr = new MemoryCommitResult();
+ synchronized (SharedPreferencesImpl.this) {
+ boolean hasListeners = mListeners.size() > 0;
+ if (hasListeners) {
+ mcr.keysModified = new ArrayList();
+ mcr.listeners =
+ new HashSet(mListeners.keySet());
+ }
+
+ synchronized (this) {
+ if (mClear) {
+ if (!mMap.isEmpty()) {
+ mChangesMade = true;
+ mMap.clear();
+ }
+ mClear = false;
+ }
+
+ for (Map.Entry e : mModified.entrySet()) {
+ String k = e.getKey();
+ Object v = e.getValue();
+ // "this" is the magic value for a removal mutation. In addition,
+ // setting a value to "null" for a given key is specified to be
+ // equivalent to calling remove on that key.
+ if (v == this || v == null) {
+ if (!mMap.containsKey(k)) {
+ continue;
+ }
+ mMap.remove(k);
+ } else {
+ if (mMap.containsKey(k)) {
+ Object existingValue = mMap.get(k);
+ if (existingValue != null && existingValue.equals(v)) {
+ continue;
+ }
+ }
+ mMap.put(k, v);
+ }
+
+ mChangesMade = true;
+ if (hasListeners) {
+ mcr.keysModified.add(k);
+ }
+ }
+
+ mModified.clear();
+ }
+ }
+ return mcr;
+ }
+
+ public boolean commit() {
+ MemoryCommitResult mcr = commitToMemory();
+ SharedPreferencesImpl.this.enqueueDiskWrite(
+ mcr, null /* sync write on this thread okay */);
+ try {
+ mcr.writtenToDiskLatch.await();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ notifyListeners(mcr);
+ return mcr.writeToDiskResult;
+ }
+
+ private void notifyListeners(final MemoryCommitResult mcr) {
+ if (mcr.listeners == null || mcr.keysModified == null ||
+ mcr.keysModified.size() == 0) {
+ return;
+ }
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
+ final String key = mcr.keysModified.get(i);
+ for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
+ if (listener != null) {
+ listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
+ }
+ }
+ }
+ } else {
+ // Run this function on the main thread.
+ mMainHandler.post(new Runnable() {
+ public void run() {
+ notifyListeners(mcr);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Enqueue an already-committed-to-memory result to be written
+ * to disk.
+ *
+ * They will be written to disk one-at-a-time in the order
+ * that they're enqueued.
+ *
+ * @param postWriteRunnable if non-null, we're being called
+ * from apply() and this is the runnable to run after
+ * the write proceeds. if null (from a regular commit()),
+ * then we're allowed to do this disk write on the main
+ * thread (which in addition to reducing allocations and
+ * creating a background thread, this has the advantage that
+ * we catch them in userdebug StrictMode reports to convert
+ * them where possible to apply() ...)
+ */
+ private void enqueueDiskWrite(final MemoryCommitResult mcr,
+ final Runnable postWriteRunnable) {
+
+ final Runnable writeToDiskRunnable = new Runnable() {
+ public void run() {
+ synchronized (SharedPreferencesImpl.this) {
+ mDiskWritesInFlight--;
+ }
+
+ synchronized (mWritingToDiskLock) {
+ writeToFile(mcr);
+ }
+
+ if (postWriteRunnable != null) {
+ postWriteRunnable.run();
+ }
+ }
+ };
+
+ final boolean isFromSyncCommit = (postWriteRunnable == null);
+
+ // Typical #commit() path with fewer allocations, doing a write on
+ // the current thread.
+ if (isFromSyncCommit) {
+ boolean wasEmpty = false;
+ synchronized (SharedPreferencesImpl.this) {
+ wasEmpty = mDiskWritesInFlight <= 0;
+ }
+
+ if (wasEmpty) {
+ synchronized (SharedPreferencesImpl.this) {
+ mDiskWritesInFlight++;
+ }
+
+ writeToDiskRunnable.run();
+ return;
+ }
+ }
+
+ synchronized (SharedPreferencesImpl.this) {
+ mDiskWritesInFlight++;
+ }
+
+ if (Build.VERSION.SDK_INT >= 26) {
+ QueuedWork.postRunnable(writeToDiskRunnable);
+ } else {
+ QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
+ }
+ }
+
+ private static FileOutputStream createFileOutputStream(File file) {
+ FileOutputStream str = null;
+ try {
+ str = new FileOutputStream(file);
+ } catch (FileNotFoundException e) {
+ File parent = file.getParentFile();
+ if (!parent.mkdir()) {
+ Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
+ return null;
+ }
+
+ FileUtils.setPermissions(parent.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH, -1, -1);
+
+ try {
+ str = new FileOutputStream(file);
+ } catch (FileNotFoundException e2) {
+ Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
+ }
+ }
+ return str;
+ }
+
+ // Note: must hold mWritingToDiskLock
+ private void writeToFile(MemoryCommitResult mcr) {
+ // Rename the current file so it may be used as a backup during the next read
+ if (mFile.exists()) {
+ final boolean changeMade;
+ synchronized (this) {
+ changeMade = mChangesMade;
+ }
+ if (!changeMade) {
+ // If the file already exists, but no changes were
+ // made to the underlying map, it's wasteful to
+ // re-write the file. Return as if we wrote it
+ // out.
+ mcr.setDiskWriteResult(true);
+ return;
+ }
+ if (!mBackupFile.exists()) {
+ if (!mFile.renameTo(mBackupFile)) {
+ Log.e(TAG, "Couldn't rename file " + mFile
+ + " to backup file " + mBackupFile);
+ mcr.setDiskWriteResult(false);
+ return;
+ }
+ } else {
+ mFile.delete();
+ }
+ }
+
+ // Attempt to write the file, delete the backup and return true as atomically as
+ // possible. If any exception occurs, delete the new file; next time we will restore
+ // from the backup.
+ try {
+ FileOutputStream str = createFileOutputStream(mFile);
+ if (str == null) {
+ mcr.setDiskWriteResult(false);
+ return;
+ }
+
+ Map copyMap;
+ synchronized (this) {
+ copyMap = new HashMap(mMap);
+ mChangesMade = false;
+ }
+
+ XmlUtils.writeMapXml(copyMap, str);
+ FileUtils.sync(str);
+ str.close();
+
+
+ int perms = FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP;
+ if ((mMode & Context.MODE_WORLD_READABLE) != 0) {
+ perms |= FileUtils.S_IROTH;
+ }
+
+ if ((mMode & Context.MODE_WORLD_WRITEABLE) != 0) {
+ perms |= FileUtils.S_IWOTH;
+ }
+
+ FileUtils.setPermissions(mFile.getPath(), perms, -1, -1);
+
+ synchronized (this) {
+ mStatTimestamp = mFile.lastModified();
+ mStatSize = mFile.length();
+ }
+
+ // Writing was successful, delete the backup file if there is one.
+ mBackupFile.delete();
+ mcr.setDiskWriteResult(true);
+ return;
+
+ } catch (Exception e1) {
+ e1.printStackTrace();
+ }
+
+ // Clean up an unsuccessfully written file
+ if (mFile.exists()) {
+ if (!mFile.delete()) {
+ Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
+ }
+ }
+ mcr.setDiskWriteResult(false);
+ }
+}
diff --git a/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/XmlUtils.java b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/XmlUtils.java
new file mode 100644
index 0000000..52c390e
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/provide/sharedpreferenceimpl/XmlUtils.java
@@ -0,0 +1,60 @@
+package com.forjrking.preferences.provide.sharedpreferenceimpl;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+
+final class XmlUtils {
+ private static Class> mClass;
+ private static Method mReadMapXmlMethod;
+ private static Method mWriteMapXmlMethod;
+
+ public static boolean init() {
+ try {
+ mClass = Class.forName("com.android.internal.util.XmlUtils");
+ mReadMapXmlMethod = mClass.getMethod("readMapXml", new Class[]{InputStream.class});
+ mWriteMapXmlMethod = mClass.getMethod("writeMapXml", new Class[]{Map.class, OutputStream.class});
+ return true;
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ public static final HashMap readMapXml(InputStream in) {
+ try {
+ return (HashMap) mReadMapXmlMethod.invoke(null, in);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ public static final void writeMapXml(Map val, OutputStream out) {
+ try {
+ mWriteMapXmlMethod.invoke(null, new Object[]{val, out});
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+}
diff --git a/preferences/src/main/java/com/forjrking/preferences/proxy/SpConfig.java b/preferences/src/main/java/com/forjrking/preferences/proxy/SpConfig.java
new file mode 100644
index 0000000..c2ecf7e
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/proxy/SpConfig.java
@@ -0,0 +1,26 @@
+package com.forjrking.preferences.proxy;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * 应用程序配置注解
+ * Created by rae on 2020-02-20.
+ */
+@Documented
+@Retention(RUNTIME)
+public @interface SpConfig {
+ /*** xml名称*/
+ String xmlName() default "";
+
+ /** DES: mmkv */
+ boolean isMMKV() default false;
+
+ /** DES: 加密key */
+ String cryptKey() default "";
+
+ /** DES: 多进程 */
+ boolean isMultiProcess() default false;
+}
\ No newline at end of file
diff --git a/preferences/src/main/java/com/forjrking/preferences/proxy/SpRetrofit.java b/preferences/src/main/java/com/forjrking/preferences/proxy/SpRetrofit.java
new file mode 100644
index 0000000..4991c26
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/proxy/SpRetrofit.java
@@ -0,0 +1,162 @@
+package com.forjrking.preferences.proxy;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+import com.forjrking.preferences.crypt.AesCrypt;
+import com.forjrking.preferences.crypt.Crypt;
+import com.forjrking.preferences.provide.ProvideKt;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import static com.forjrking.preferences.kt.bindings.PutValueExtKt.getSerializer;
+
+/**
+ * 应用程序代理类
+ * Created by rae on 2020-02-20.
+ */
+public final class SpRetrofit {
+
+ private SpRetrofit() {
+ }
+
+ /**
+ * 创建程序配置代理类
+ *
+ * @param cls 类的Class
+ */
+ @SuppressWarnings("unchecked")
+ public static T create(Context context, Class cls) {
+ SpConfig config = cls.getAnnotation(SpConfig.class);
+ if (config == null) {
+ throw new RuntimeException("请在配置类标注@SpConfig()");
+ }
+ if (!cls.isInterface()) {
+ throw new RuntimeException("配置类必须是接口");
+ }
+ String configName = config.xmlName();
+ if (TextUtils.isEmpty(configName)) {
+ configName = cls.getName();
+ }
+ String cryptKey = config.cryptKey();
+ if (TextUtils.isEmpty(cryptKey)) {
+ cryptKey = null;
+ }
+ Crypt mCrypt = null;
+ if (!TextUtils.isEmpty(cryptKey) && !config.isMMKV()) {
+ mCrypt = new AesCrypt(cryptKey);
+ }
+ SharedPreferences preferences = ProvideKt.createSharedPreferences(context, configName, cryptKey, config.isMultiProcess(), config.isMMKV());
+ // 创建动态代理
+ return (T) Proxy.newProxyInstance(cls.getClassLoader(), new Class>[]{cls}, new sharePreferencesProxy(preferences, mCrypt));
+ }
+
+ private static class sharePreferencesProxy implements InvocationHandler {
+
+ private final SharedPreferences mPreference;
+ private final Crypt mCrypt;
+
+ private sharePreferencesProxy(SharedPreferences preference, Crypt crypt) {
+ this.mPreference = preference;
+ this.mCrypt = crypt;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ String methodName = method.getName().toUpperCase();
+ // 清除配置文件
+ if (methodName.equalsIgnoreCase("CLEAR")) {
+ mPreference.edit().clear().apply();
+ }
+ // 移除配置项处理
+ else if (methodName.equalsIgnoreCase("REMOVE") && args != null) {
+ String key = args[0].toString().toUpperCase();
+ mPreference.edit().remove(key).apply();
+ }
+ // Get方法处理
+ else if (methodName.startsWith("SET")) {
+ setValue(methodName.replaceFirst("SET", ""), method, args);
+ }
+ // Set方法处理
+ else if (methodName.startsWith("GET")) {
+ return getValue(methodName.replaceFirst("GET", ""), method, args);
+ }
+ // Is方法处理,比如:isLogin()、isVip(),这类的布尔值
+ else if (methodName.startsWith("IS")) {
+ boolean value = mPreference.getBoolean(methodName.replaceFirst("IS", ""), false);
+ return value;
+ }
+ return null;
+ }
+
+ /**
+ * 设置配置值
+ */
+ private void setValue(String name, Method method, Object[] args) {
+ if (args.length != 1) throw new IllegalArgumentException("set方法的方法参数只允许一个");
+ Class>[] parameterTypes = method.getParameterTypes();
+ Class> parameterType = parameterTypes[0];
+ Object arg = args[0];
+ SharedPreferences.Editor editor = mPreference.edit();
+ if (parameterType == String.class) {
+ String string = (String) arg;
+ if (mCrypt != null && !TextUtils.isEmpty(string)) {
+ string = mCrypt.encrypt(string);
+ }
+ editor.putString(name, string);
+ } else if (parameterType == int.class) {
+ editor.putInt(name, (int) arg);
+ } else if (parameterType == boolean.class) {
+ editor.putBoolean(name, (boolean) arg);
+ } else if (parameterType == float.class) {
+ editor.putFloat(name, (float) arg);
+ } else if (parameterType == long.class) {
+ editor.putLong(name, (long) arg);
+ } else {
+ // 其他值默认使用Json字符串
+ String json = getSerializer().serialize(arg);
+ if (mCrypt != null && !TextUtils.isEmpty(json)) {
+ json = mCrypt.encrypt(json);
+ }
+ editor.putString(name, json);
+ }
+ editor.apply();
+ }
+
+ /**
+ * 获取配置值
+ */
+ private Object getValue(String name, Method method, Object[] args) {
+ Class> type = method.getReturnType();
+ Object defaultValue = args == null ? null : args[0];
+ if (type == String.class) {
+ String string = mPreference.getString(name, (String) defaultValue);
+ if (mCrypt != null && !TextUtils.isEmpty(string)) {
+ string = mCrypt.decrypt(string);
+ if (TextUtils.isEmpty(string)) {
+ string = (String) defaultValue;
+ }
+ }
+ return string;
+ } else if (type == int.class) {
+ return mPreference.getInt(name, defaultValue == null ? 0 : (int) defaultValue);
+ } else if (type == boolean.class) {
+ return mPreference.getBoolean(name, defaultValue != null && (boolean) defaultValue);
+ } else if (type == float.class) {
+ return mPreference.getFloat(name, defaultValue == null ? 0 : (float) defaultValue);
+ } else if (type == long.class) {
+ return mPreference.getLong(name, defaultValue == null ? 0 : (long) defaultValue);
+ } else {
+ // 其他值默认使用Json字符串
+ String json = mPreference.getString(name, null);
+ if (mCrypt != null && !TextUtils.isEmpty(json)) {
+ json = mCrypt.decrypt(json);
+ }
+ return getSerializer().deserialize(json, type);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/preferences/src/main/java/com/forjrking/preferences/serialize/GsonSerializer.kt b/preferences/src/main/java/com/forjrking/preferences/serialize/GsonSerializer.kt
new file mode 100644
index 0000000..c7958aa
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/serialize/GsonSerializer.kt
@@ -0,0 +1,16 @@
+package com.forjrking.preferences.serialize
+
+import com.google.gson.Gson
+import java.lang.RuntimeException
+import java.lang.reflect.Type
+
+class GsonSerializer(val gson: Gson) : Serializer {
+
+ override fun serialize(toSerialize: Any?): String? = gson.toJson(toSerialize)
+
+ override fun deserialize(serialized: String?, type: Type): Any? = try {
+ gson.fromJson(serialized, type)
+ } catch (e: Throwable) {
+ throw RuntimeException("Error in parsing to $type. The string to parse: \"$this\"", e)
+ }
+}
\ No newline at end of file
diff --git a/preferences/src/main/java/com/forjrking/preferences/serialize/Serializer.kt b/preferences/src/main/java/com/forjrking/preferences/serialize/Serializer.kt
new file mode 100644
index 0000000..cf070d2
--- /dev/null
+++ b/preferences/src/main/java/com/forjrking/preferences/serialize/Serializer.kt
@@ -0,0 +1,10 @@
+package com.forjrking.preferences.serialize
+
+import java.lang.reflect.Type
+
+interface Serializer {
+
+ fun serialize(toSerialize: Any?): String?
+
+ fun deserialize(serialized: String?, type: Type): Any?
+}
diff --git a/preferences/src/test/java/com/forjrking/preferences/ExampleUnitTest.kt b/preferences/src/test/java/com/forjrking/preferences/ExampleUnitTest.kt
new file mode 100644
index 0000000..b4335a0
--- /dev/null
+++ b/preferences/src/test/java/com/forjrking/preferences/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.forjrking.preferences
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..99fb1b1
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+include ':preferences'
+include ':app'
\ No newline at end of file