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:
Steve Kondik
2016-10-14 21:24:54 -07:00
parent 0dae635abe
commit 3805419c29
7 changed files with 513 additions and 13 deletions

View 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;
}
}

View 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);
}
}
}
});
}
}
}
}
};
}

View 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);
}
}

View File

@@ -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) {