Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#22 Added intent-bridged preferences. #25

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 62 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand All @@ -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`:

Expand All @@ -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:

Expand All @@ -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
<receiver
android:name=".MyPreferencesRequestedReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.app.preferences.PREFERENCES_REQUESTED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
```

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.crossbowffs.remotepreferences;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;

/**
* <p>
* Propagates changes in the given {@link SharedPreferences} as intents
* to any other app on the device.
* </p>
*
* <p>
* You must extend this class and declare a 0-argument constructor which
* calls the super constructor with the appropriate action and preferences
* parameters.
* </p>
*
* <p>
* 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.
* </p>
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
* Provides a {@link SharedPreferences} compatible API to
* {@link IntentBridgedPreferenceSender} {@link IntentBridgedPreferencesRequestedReceiver}.
* See both classes for more information.
* </p>
*
* <p>
* 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.
* </p>
*/
public class IntentBridgedPreferences implements SharedPreferences {
private final SharedPreferences mCachedPreferences;
private final IntentFilter mPreferencesIntentFilter;
private final IntentBridgedPreferencesReceiver mPreferencesReceiver;

/**
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* @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<String, ?> getAll() {
return mCachedPreferences.getAll();
}

@Override
public String getString(String key, String defaultValue) {
return mCachedPreferences.getString(key, defaultValue);
}

@Override
public Set<String> getStringSet(String key, Set<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
* Receives preferences as intents and sets the new value(s) into
* the given {@link SharedPreferences} insance.
* </p>
*/
/* 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
* Receives requests to send the preferences and responds with
* the full set of preferences being sent as intent.
* </p>
*/
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<String, ?> preference : mSourcePreferences.getAll().entrySet()) {
IntentBridgedUtils.setAsIntentExtra(preferencesIntent, preference.getKey(), preference.getValue());
}

context.sendBroadcast(preferencesIntent);
}
}
Loading