cmsdk: RemotePreference API
* Factoring out the work done for CMParts into an actual API that can be used for all of the various device settings apps. Change-Id: Ie1b47c900c2b37457b90f1b0af0634d5fe12fd9a
This commit is contained in:
180
sdk/src/java/cyanogenmod/preference/RemotePreference.java
Normal file
180
sdk/src/java/cyanogenmod/preference/RemotePreference.java
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The CyanogenMod Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cyanogenmod.preference;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserHandle;
|
||||
import android.support.v7.preference.R;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A RemotePreference is a view into preference logic which lives in another
|
||||
* process. The primary use case for this is at the platform level where
|
||||
* many applications may be contributing their preferences into the
|
||||
* Settings app.
|
||||
*
|
||||
* A RemotePreference appears as a PreferenceScreen and redirects to
|
||||
* the real application when clicked. The remote application can
|
||||
* send events back to the preference when data changes and the view
|
||||
* needs to be updated. See RemotePreferenceUpdater for a base class
|
||||
* to use on the application side which implements the listeners and
|
||||
* protocol.
|
||||
*
|
||||
* The interprocess communication is realized using BroadcastReceivers.
|
||||
* When the application wants to update the RemotePreference with
|
||||
* new data, it sends an ACTION_REFRESH_PREFERENCE with a particular
|
||||
* Uri. The RemotePreference listens while attached, and performs
|
||||
* an ordered broadcast with ACTION_UPDATE_PREFERENCE back to
|
||||
* the application, which is then returned to the preference after
|
||||
* being filled with new data.
|
||||
*
|
||||
* The external activity should include the META_REMOTE_RECEIVER
|
||||
* and (optionally) the META_REMOTE_KEY strings in it's metadata.
|
||||
* META_REMOTE_RECEIVER must contain the class name of the
|
||||
* RemotePreferenceUpdater which we should request updates from.
|
||||
* META_REMOTE_KEY must contain the key used by the preference
|
||||
* which should match on both sides.
|
||||
*/
|
||||
public class RemotePreference extends SelfRemovingPreference
|
||||
implements RemotePreferenceManager.OnRemoteUpdateListener {
|
||||
|
||||
private static final String TAG = RemotePreference.class.getSimpleName();
|
||||
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
|
||||
|
||||
public static final String ACTION_REFRESH_PREFERENCE =
|
||||
"cyanogenmod.intent.action.REFRESH_PREFERENCE";
|
||||
|
||||
public static final String ACTION_UPDATE_PREFERENCE =
|
||||
"cyanogenmod.intent.action.UPDATE_PREFERENCE";
|
||||
|
||||
public static final String META_REMOTE_RECEIVER =
|
||||
"org.cyanogenmod.settings.summary.receiver";
|
||||
|
||||
public static final String META_REMOTE_KEY =
|
||||
"org.cyanogenmod.settings.summary.key";
|
||||
|
||||
public static final String EXTRA_ENABLED = ":cm:pref_enabled";
|
||||
public static final String EXTRA_KEY = ":cm:pref_key";
|
||||
public static final String EXTRA_SUMMARY = ":cm:pref_summary";
|
||||
|
||||
protected final Context mContext;
|
||||
|
||||
public RemotePreference(Context context, AttributeSet attrs,
|
||||
int defStyle, int defStyleRes) {
|
||||
super(context, attrs, defStyle, defStyleRes);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public RemotePreference(Context context, AttributeSet attrs, int defStyle) {
|
||||
this(context, attrs, defStyle, 0);
|
||||
}
|
||||
|
||||
public RemotePreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, ConstraintsHelper.getAttr(
|
||||
context, R.attr.preferenceScreenStyle, android.R.attr.preferenceScreenStyle));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteUpdated(Bundle bundle) {
|
||||
if (DEBUG) Log.d(TAG, "onRemoteUpdated: " + bundle.toString());
|
||||
|
||||
if (bundle.containsKey(EXTRA_ENABLED)) {
|
||||
boolean available = bundle.getBoolean(EXTRA_ENABLED, true);
|
||||
if (available != isAvailable()) {
|
||||
setAvailable(available);
|
||||
}
|
||||
}
|
||||
if (isAvailable()) {
|
||||
setSummary(bundle.getString(EXTRA_SUMMARY));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttached() {
|
||||
super.onAttached();
|
||||
if (isAvailable()) {
|
||||
RemotePreferenceManager.get(mContext).attach(getKey(), this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetached() {
|
||||
super.onDetached();
|
||||
RemotePreferenceManager.get(mContext).detach(getKey());
|
||||
}
|
||||
|
||||
protected String getRemoteKey(Bundle metaData) {
|
||||
String remoteKey = metaData.getString(META_REMOTE_KEY);
|
||||
return (remoteKey == null || !remoteKey.equals(getKey())) ? null : remoteKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent getReceiverIntent() {
|
||||
final Intent i = getIntent();
|
||||
if (i == null) {
|
||||
Log.w(TAG, "No target intent specified in preference!");
|
||||
return null;
|
||||
}
|
||||
|
||||
PackageManager pm = mContext.getPackageManager();
|
||||
List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(i,
|
||||
PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA,
|
||||
UserHandle.myUserId());
|
||||
|
||||
|
||||
if (results.size() == 0) {
|
||||
Log.w(TAG, "No activity found for: " + Objects.toString(i));
|
||||
}
|
||||
|
||||
for (ResolveInfo resolved : results) {
|
||||
ActivityInfo info = resolved.activityInfo;
|
||||
Log.d(TAG, "ResolveInfo " + Objects.toString(resolved));
|
||||
|
||||
Bundle meta = info.metaData;
|
||||
if (meta == null || !meta.containsKey(META_REMOTE_RECEIVER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String receiverClass = meta.getString(META_REMOTE_RECEIVER);
|
||||
String receiverPackage = info.packageName;
|
||||
String remoteKey = getRemoteKey(meta);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "getReceiverIntent class=" + receiverClass +
|
||||
" package=" + receiverPackage + " key=" + remoteKey);
|
||||
|
||||
if (remoteKey == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Intent ri = new Intent(ACTION_UPDATE_PREFERENCE);
|
||||
ri.setComponent(new ComponentName(receiverPackage, receiverClass));
|
||||
ri.putExtra(EXTRA_KEY, remoteKey);
|
||||
return ri;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
165
sdk/src/java/cyanogenmod/preference/RemotePreferenceManager.java
Normal file
165
sdk/src/java/cyanogenmod/preference/RemotePreferenceManager.java
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The CyanogenMod Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cyanogenmod.preference;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.UserHandle;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import cyanogenmod.platform.Manifest;
|
||||
|
||||
import static cyanogenmod.preference.RemotePreference.ACTION_REFRESH_PREFERENCE;
|
||||
import static cyanogenmod.preference.RemotePreference.ACTION_UPDATE_PREFERENCE;
|
||||
import static cyanogenmod.preference.RemotePreference.EXTRA_KEY;
|
||||
|
||||
/**
|
||||
* Manages attaching and detaching of RemotePreferences and optimizes callbacks
|
||||
* thru a single receiver on a separate thread.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class RemotePreferenceManager {
|
||||
|
||||
private static final String TAG = RemotePreferenceManager.class.getSimpleName();
|
||||
|
||||
private static final boolean DEBUG = Log.isLoggable(
|
||||
RemotePreference.class.getSimpleName(), Log.VERBOSE);
|
||||
|
||||
private static RemotePreferenceManager sInstance;
|
||||
|
||||
private final Context mContext;
|
||||
private final Map<String, Intent> mCache = new ArrayMap<>();
|
||||
private final Map<String, OnRemoteUpdateListener> mCallbacks = new ArrayMap<>();
|
||||
|
||||
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private Handler mHandler;
|
||||
private HandlerThread mThread;
|
||||
|
||||
public interface OnRemoteUpdateListener {
|
||||
public Intent getReceiverIntent();
|
||||
|
||||
public void onRemoteUpdated(Bundle bundle);
|
||||
}
|
||||
|
||||
private RemotePreferenceManager(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public synchronized static RemotePreferenceManager get(Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new RemotePreferenceManager(context);
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public void attach(String key, OnRemoteUpdateListener pref) {
|
||||
Intent i;
|
||||
synchronized (mCache) {
|
||||
i = mCache.get(key);
|
||||
if (i == null && !mCache.containsKey(key)) {
|
||||
i = pref.getReceiverIntent();
|
||||
mCache.put(key, i);
|
||||
}
|
||||
}
|
||||
synchronized (mCallbacks) {
|
||||
if (i != null) {
|
||||
mCallbacks.put(key, pref);
|
||||
if (mCallbacks.size() == 1) {
|
||||
mThread = new HandlerThread("RemotePreference");
|
||||
mThread.start();
|
||||
mHandler = new Handler(mThread.getLooper());
|
||||
mContext.registerReceiver(mListener,
|
||||
new IntentFilter(ACTION_REFRESH_PREFERENCE),
|
||||
Manifest.permission.MANAGE_REMOTE_PREFERENCES, mHandler);
|
||||
}
|
||||
requestUpdate(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void detach(String key) {
|
||||
synchronized (mCallbacks) {
|
||||
if (mCallbacks.remove(key) != null && mCallbacks.size() == 0) {
|
||||
mContext.unregisterReceiver(mListener);
|
||||
if (mThread != null) {
|
||||
mThread.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requestUpdate(String key) {
|
||||
synchronized (mCache) {
|
||||
Intent i = mCache.get(key);
|
||||
if (i == null) {
|
||||
return;
|
||||
}
|
||||
mContext.sendOrderedBroadcastAsUser(i, UserHandle.CURRENT,
|
||||
Manifest.permission.MANAGE_REMOTE_PREFERENCES,
|
||||
mListener, mHandler, Activity.RESULT_OK, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mListener = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onReceive: intent=" + Objects.toString(intent));
|
||||
|
||||
if (ACTION_REFRESH_PREFERENCE.equals(intent.getAction())) {
|
||||
final String key = intent.getStringExtra(EXTRA_KEY);
|
||||
synchronized (mCallbacks) {
|
||||
if (key != null && mCallbacks.containsKey(key)) {
|
||||
requestUpdate(key);
|
||||
}
|
||||
}
|
||||
} else if (ACTION_UPDATE_PREFERENCE.equals(intent.getAction())) {
|
||||
if (getAbortBroadcast()) {
|
||||
Log.e(TAG, "Broadcast aborted, code=" + getResultCode());
|
||||
return;
|
||||
}
|
||||
final Bundle bundle = getResultExtras(true);
|
||||
final String key = bundle.getString(EXTRA_KEY);
|
||||
synchronized (mCallbacks) {
|
||||
if (key != null && mCallbacks.containsKey(key)) {
|
||||
mMainHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mCallbacks) {
|
||||
if (mCallbacks.containsKey(key)) {
|
||||
mCallbacks.get(key).onRemoteUpdated(bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
129
sdk/src/java/cyanogenmod/preference/RemotePreferenceUpdater.java
Normal file
129
sdk/src/java/cyanogenmod/preference/RemotePreferenceUpdater.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The CyanogenMod Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cyanogenmod.preference;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserHandle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import cyanogenmod.platform.Manifest;
|
||||
|
||||
/**
|
||||
* Base class for remote summary providers.
|
||||
* <p>
|
||||
* When an application is hosting preferences which are served by a different process,
|
||||
* the former needs to stay updated with changes in order to display the correct
|
||||
* summary when the user returns to the latter.
|
||||
* <p>
|
||||
* This class implements a simple ordered broadcast mechanism where the application
|
||||
* running the RemotePreference sends an explicit broadcast to the host, who
|
||||
* fills out the extras in the result bundle and returns it to the caller.
|
||||
* <p>
|
||||
* A minimal implementation will override getSummary and return a summary
|
||||
* for the given key. Alternatively, fillResultExtras can be overridden
|
||||
* if additional data should be added to the result.
|
||||
*/
|
||||
public class RemotePreferenceUpdater extends BroadcastReceiver {
|
||||
|
||||
private static final String TAG = RemotePreferenceUpdater.class.getSimpleName();
|
||||
|
||||
private static final boolean DEBUG = Log.isLoggable(
|
||||
RemotePreference.class.getSimpleName(), Log.VERBOSE);
|
||||
|
||||
private static Intent getTargetIntent(Context context, String key) {
|
||||
final Intent i = new Intent(RemotePreference.ACTION_REFRESH_PREFERENCE);
|
||||
i.putExtra(RemotePreference.EXTRA_KEY, key);
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the updated summary for the given key
|
||||
*
|
||||
* @param key
|
||||
* @return the summary for the given key
|
||||
*/
|
||||
protected String getSummary(Context context, String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fill the bundle with the summary and any other data needed to update
|
||||
* the client.
|
||||
*
|
||||
* @param context
|
||||
* @param key
|
||||
* @param extras
|
||||
* @return true if successful
|
||||
*/
|
||||
protected boolean fillResultExtras(Context context, String key, Bundle extras) {
|
||||
extras.putString(RemotePreference.EXTRA_KEY, key);
|
||||
|
||||
final String summary = getSummary(context, key);
|
||||
if (summary == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
extras.putString(RemotePreference.EXTRA_SUMMARY, summary);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (isOrderedBroadcast() &&
|
||||
RemotePreference.ACTION_UPDATE_PREFERENCE.equals(intent.getAction())) {
|
||||
final String key = intent.getStringExtra(RemotePreference.EXTRA_KEY);
|
||||
if (DEBUG) Log.d(TAG, "onReceive key=" +key +
|
||||
" intent=" + Objects.toString(intent) +
|
||||
" extras=" + Objects.toString(intent.getExtras()));
|
||||
|
||||
if (key != null) {
|
||||
if (fillResultExtras(context, key, getResultExtras(true))) {
|
||||
setResultCode(Activity.RESULT_OK);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "onReceive result=" +
|
||||
Objects.toString(getResultExtras(true)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the RemotePreference that updated state is available. Call from
|
||||
* the fragment when necessary.
|
||||
*
|
||||
* @param context
|
||||
* @param key
|
||||
*/
|
||||
public static void notifyChanged(Context context, String key) {
|
||||
if (DEBUG) Log.d(TAG, "notifyChanged: key=" + key +
|
||||
" target=" + Objects.toString(getTargetIntent(context, key)));
|
||||
context.sendBroadcastAsUser(getTargetIntent(context, key),
|
||||
UserHandle.CURRENT, Manifest.permission.MANAGE_REMOTE_PREFERENCES);
|
||||
}
|
||||
}
|
||||
@@ -237,7 +237,7 @@ public class PartsList {
|
||||
if (mRemotes.size() == 0) {
|
||||
final IntentFilter filter = new IntentFilter(ACTION_PART_CHANGED);
|
||||
mContext.registerReceiver(mPartChangedReceiver, filter,
|
||||
Manifest.permission.MANAGE_PARTS, null);
|
||||
Manifest.permission.MANAGE_REMOTE_PREFERENCES, null);
|
||||
}
|
||||
|
||||
Set<PartInfo.RemotePart> remotes = mRemotes.get(key);
|
||||
@@ -255,7 +255,7 @@ public class PartsList {
|
||||
// Send an ordered broadcast to request a refresh and receive the reply
|
||||
// on the BroadcastReceiver.
|
||||
mContext.sendOrderedBroadcastAsUser(i, UserHandle.CURRENT,
|
||||
Manifest.permission.MANAGE_PARTS,
|
||||
Manifest.permission.MANAGE_REMOTE_PREFERENCES,
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
|
||||
Reference in New Issue
Block a user