diff --git a/README.md b/README.md index a535782..6cc371f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A drop-in solution for inter-app access to `SharedPreferences`. ## Installation -1\. Add the dependency to your `build.gradle` file: +Add the dependency to your `build.gradle` file: ``` repositories { @@ -17,7 +17,10 @@ dependencies { } ``` -2\. Subclass `RemotePreferenceProvider` and implement a 0-argument + +## RemotePreferences using Content Providers + +1\. Subclass `RemotePreferenceProvider` and implement a 0-argument constructor which calls the super constructor with an authority (e.g. `"com.example.app.preferences"`) and an array of preference files to expose: @@ -30,7 +33,7 @@ public class MyPreferenceProvider extends RemotePreferenceProvider { } ``` -3\. Add the corresponding entry to `AndroidManifest.xml`, with +2\. Add the corresponding entry to `AndroidManifest.xml`, with `android:authorities` equal to the authority you picked in the last step, and `android:exported` set to `true`: @@ -41,7 +44,7 @@ last step, and `android:exported` set to `true`: android:exported="true"/> ``` -4\. You're all set! To access your preferences, create a new +3\. You're all set! To access your preferences, create a new instance of `RemotePreferences` with the same authority and the name of the preference file: @@ -61,6 +64,61 @@ if your code is executing within the app that owns the preferences. Only use Also note that your preference keys cannot be `null` or `""` (empty string). +On Android 11 and above the receiving app must specify the contents it reads +in it's `AndroidManifest.xml`. + + +## IntentBridgedPreferences using Intents + +1\. Subclass `IntentBridgedPreferencesRequestedReceiver` and implement +a 0-argument constructor which calls the super constructor with an action +(e.g. "com.example.app.preferences") and the `SharedPreferences` instance to +expose: + +```Java +public class MyPreferencesRequestedReceiver extends IntenBridgedPreferencesRequestedReceiver { + public MyPreferencesRequestedReceiver() { + super("com.example.app.preferences", PreferenceManager.getDefaultSharedPreferences(AndroidAppHelper.currentApplication())); + } +} +``` + +2\. Add the corresponding entry to `AndroidManifest.xml`, with +`action` equal to the action you picked in the last step: + +```XML + + + + + + +``` + +3\. If you want changes to propagate immediately, you need to register +an `IntentBridgedPreferenceSender` on the preferences you want to send: + +```java +IntentBridgedPreferenceSender preferenceSender = new IntentBridgedPreferenceSender( + getContext(), + "com.example.app.preferences", + getPreferenceManager().getSharedPreferences()); +``` + +Don't forget to hold onto the created instance (e.g. as a member of +the activity), as the preference listeners are all weak instances. + +4\. You're all set! To access your preferences, create a new +instance of `IntentBridgedPreferences` with the same authority: + +```Java +SharedPreferences prefs = new IntentBridgedPreferences(context, "com.example.app.preferences"); +int value = prefs.getInt("my_int_pref", 0); +``` + ## Security diff --git a/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferenceSender.java b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferenceSender.java new file mode 100644 index 0000000..87e0b2f --- /dev/null +++ b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferenceSender.java @@ -0,0 +1,53 @@ +package com.crossbowffs.remotepreferences; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +/** + *

+ * Propagates changes in the given {@link SharedPreferences} as intents + * to any other app on the device. + *

+ * + *

+ * You must extend this class and declare a 0-argument constructor which + * calls the super constructor with the appropriate action and preferences + * parameters. + *

+ * + *

+ * Remember to hold onto the created instance explicitely, e.g. through + * a member of an activity. This registers a listener on the given + * {@link SharedPreferences} and these are only weakly referenced. + *

+ */ +public class IntentBridgedPreferenceSender { + private final String mActionName; + private final Context mContext; + private final SharedPreferences.OnSharedPreferenceChangeListener mListener; + + /** + * Initializes this sender with the specified action and the given + * {@link SharedPreferences} as sources for changes and preferences. + * + * @param context The {@link Context} of this app. + * @param actionName The actionName of the action. + * @param sourcePreferences The {@link SharedPreferences} used as source. + */ + public IntentBridgedPreferenceSender(Context context, String actionName, SharedPreferences sourcePreferences) { + mContext = context; + mActionName = actionName; + mListener = this::onPreferenceChanged; + + sourcePreferences.registerOnSharedPreferenceChangeListener(mListener); + } + + private void onPreferenceChanged(SharedPreferences sharedPreferences, String key) { + Intent intent = new Intent(mActionName + ".PREFERENCES"); + + IntentBridgedUtils.setAsIntentExtra(intent, key, sharedPreferences.getAll().get(key)); + + mContext.sendBroadcast(intent); + } +} diff --git a/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferences.java b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferences.java new file mode 100644 index 0000000..72a57c0 --- /dev/null +++ b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferences.java @@ -0,0 +1,124 @@ +package com.crossbowffs.remotepreferences; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Build; + +import java.util.Map; +import java.util.Set; + +/** + *

+ * Provides a {@link SharedPreferences} compatible API to + * {@link IntentBridgedPreferenceSender} {@link IntentBridgedPreferencesRequestedReceiver}. + * See both classes for more information. + *

+ * + *

+ * If you are reading preferences from the same context as the + * provider, you should not use this class; just access the + * {@link SharedPreferences} API as you would normally. + *

+ */ +public class IntentBridgedPreferences implements SharedPreferences { + private final SharedPreferences mCachedPreferences; + private final IntentFilter mPreferencesIntentFilter; + private final IntentBridgedPreferencesReceiver mPreferencesReceiver; + + /** + *

+ * Initializes the intent bridged preferences with the specified + * authority. The authority must match the action tag defined in + * your manifest file. Only the specified preferences will be + * accessible through the provider. + *

+ * + *

+ * As intents are asynchronous, this instance keeps a local cache + * of the last known values of the preferences. Upon creation + * a request to refresh this cache will be send, so the (old) cached + * values will be used until the new set of preferences is received. + *

+ * + * @param context The {@link Context} of this app. + * @param actionName The actionName of the action. + */ + public IntentBridgedPreferences(Context context, String actionName) { + mCachedPreferences = context.getSharedPreferences(actionName, Context.MODE_PRIVATE); + mPreferencesIntentFilter = new IntentFilter(actionName + ".PREFERENCES"); + mPreferencesReceiver = new IntentBridgedPreferencesReceiver(mCachedPreferences); + + context.registerReceiver(mPreferencesReceiver, mPreferencesIntentFilter); + + Intent intent = new Intent(actionName + ".PREFERENCES_REQUESTED") + .addCategory(Intent.CATEGORY_DEFAULT) + .addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT) { + intent = intent.setPackage(actionName); + } + + context.sendBroadcast(intent); + } + + @Override + public Map getAll() { + return mCachedPreferences.getAll(); + } + + @Override + public String getString(String key, String defaultValue) { + return mCachedPreferences.getString(key, defaultValue); + } + + @Override + public Set getStringSet(String key, Set defaultValue) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + return mCachedPreferences.getStringSet(key, defaultValue); + } else { + return null; + } + } + + @Override + public int getInt(String key, int defaultValue) { + return mCachedPreferences.getInt(key, defaultValue); + } + + @Override + public long getLong(String key, long defaultValue) { + return mCachedPreferences.getLong(key, defaultValue); + } + + @Override + public float getFloat(String key, float defaultValue) { + return mCachedPreferences.getFloat(key, defaultValue); + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + return mCachedPreferences.getBoolean(key, defaultValue); + } + + @Override + public boolean contains(String key) { + return mCachedPreferences.contains(key); + } + + @Override + public Editor edit() { + return mCachedPreferences.edit(); + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) { + mCachedPreferences.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) { + mCachedPreferences.unregisterOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener); + } +} diff --git a/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferencesReceiver.java b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferencesReceiver.java new file mode 100644 index 0000000..6e6eb6a --- /dev/null +++ b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferencesReceiver.java @@ -0,0 +1,58 @@ +package com.crossbowffs.remotepreferences; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; + +import java.util.Arrays; +import java.util.HashSet; + +/** + *

+ * Receives preferences as intents and sets the new value(s) into + * the given {@link SharedPreferences} insance. + *

+ */ +/* package */ class IntentBridgedPreferencesReceiver extends BroadcastReceiver { + private final SharedPreferences mSharedPreferences; + + public IntentBridgedPreferencesReceiver(SharedPreferences targetPreferences) { + mSharedPreferences = targetPreferences; + } + + @Override + public void onReceive(Context context, Intent intent) { + Bundle extras = intent.getExtras(); + + if (extras != null) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + + for (String key : extras.keySet()) { + Object value = extras.get(key); + + if (value instanceof String[]) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + editor.putStringSet(key, new HashSet<>(Arrays.asList((String[]) value))); + } + } else if (value instanceof String) { + editor.putString(key, (String) value); + } else if (value instanceof Long) { + editor.putLong(key, (long) value); + } else if (value instanceof Integer) { + editor.putInt(key, (int) value); + } else if (value instanceof Float) { + editor.putFloat(key, (float) value); + } else if (value instanceof Boolean) { + editor.putBoolean(key, (boolean) value); + } else if (value == null) { + editor.putString(key, (String) null); + } + } + + editor.commit(); + } + } +} diff --git a/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferencesRequestedReceiver.java b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferencesRequestedReceiver.java new file mode 100644 index 0000000..0c24fc8 --- /dev/null +++ b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedPreferencesRequestedReceiver.java @@ -0,0 +1,42 @@ +package com.crossbowffs.remotepreferences; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import java.util.Map; + +/** + *

+ * Receives requests to send the preferences and responds with + * the full set of preferences being sent as intent. + *

+ */ +public abstract class IntentBridgedPreferencesRequestedReceiver extends BroadcastReceiver { + private final String mActionName; + private final SharedPreferences mSourcePreferences; + + /** + * Initializes this receiver with the action name and + * the given {@link SharedPreferences} as source for preferences. + * + * @param actionName The actionName of the action. + * @param sourcePreferences The {@link SharedPreferences} used as source. + */ + public IntentBridgedPreferencesRequestedReceiver(String actionName, SharedPreferences sourcePreferences) { + mActionName = actionName; + mSourcePreferences = sourcePreferences; + } + + @Override + public void onReceive(Context context, Intent receivedIntent) { + Intent preferencesIntent = new Intent(mActionName + ".PREFERENCES"); + + for (Map.Entry preference : mSourcePreferences.getAll().entrySet()) { + IntentBridgedUtils.setAsIntentExtra(preferencesIntent, preference.getKey(), preference.getValue()); + } + + context.sendBroadcast(preferencesIntent); + } +} diff --git a/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedUtils.java b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedUtils.java new file mode 100644 index 0000000..6787f6f --- /dev/null +++ b/library/src/main/java/com/crossbowffs/remotepreferences/IntentBridgedUtils.java @@ -0,0 +1,40 @@ +package com.crossbowffs.remotepreferences; + +import android.content.Intent; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +/** + * Common utilities used to set preferences into intents + * and extract them again. + */ +/* package */ final class IntentBridgedUtils { + private IntentBridgedUtils() {} + + /** + * Sets the parameter as an extra on the given intent. + * + * @param intent The target {@link Intent}. + * @param key The key of the value. + * @param value The value, as type {@link Object}. + */ + public static void setAsIntentExtra(Intent intent, String key, Object value) { + if (value instanceof String) { + intent.putExtra(key, (String)value); + } else if (value instanceof Set) { + intent.putExtra(key, ((Set)value).toArray(new String[0])); + } else if (value instanceof Integer) { + intent.putExtra(key, (int)value); + } else if (value instanceof Long) { + intent.putExtra(key, (long)value); + } else if (value instanceof Float) { + intent.putExtra(key, (float)value); + } else if (value instanceof Boolean) { + intent.putExtra(key, (boolean)value); + } else { + intent.putExtra(key, (Serializable) null); + } + } +}