From 3805419c291b69298ad664ed8a174f6f98200089 Mon Sep 17 00:00:00 2001 From: Steve Kondik Date: Fri, 14 Oct 2016 21:24:54 -0700 Subject: [PATCH] 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 --- api/cm_current.txt | 29 ++- cm/res/AndroidManifest.xml | 13 +- cm/res/res/values/strings.xml | 6 +- .../preference/RemotePreference.java | 180 ++++++++++++++++++ .../preference/RemotePreferenceManager.java | 165 ++++++++++++++++ .../preference/RemotePreferenceUpdater.java | 129 +++++++++++++ .../internal/cmparts/PartsList.java | 4 +- 7 files changed, 513 insertions(+), 13 deletions(-) create mode 100644 sdk/src/java/cyanogenmod/preference/RemotePreference.java create mode 100644 sdk/src/java/cyanogenmod/preference/RemotePreferenceManager.java create mode 100644 sdk/src/java/cyanogenmod/preference/RemotePreferenceUpdater.java diff --git a/api/cm_current.txt b/api/cm_current.txt index 85a4aca4..294ce636 100644 --- a/api/cm_current.txt +++ b/api/cm_current.txt @@ -705,8 +705,8 @@ package cyanogenmod.platform { field public static final java.lang.String LIVE_LOCK_SCREEN_MANAGER_ACCESS = "cyanogenmod.permission.LIVE_LOCK_SCREEN_MANAGER_ACCESS"; field public static final java.lang.String MANAGE_ALARMS = "cyanogenmod.permission.MANAGE_ALARMS"; field public static final java.lang.String MANAGE_LIVEDISPLAY = "cyanogenmod.permission.MANAGE_LIVEDISPLAY"; - field public static final java.lang.String MANAGE_PARTS = "cyanogenmod.permission.MANAGE_PARTS"; field public static final java.lang.String MANAGE_PERSISTENT_STORAGE = "cyanogenmod.permission.MANAGE_PERSISTENT_STORAGE"; + field public static final java.lang.String MANAGE_REMOTE_PREFERENCES = "cyanogenmod.permission.MANAGE_REMOTE_PREFERENCES"; field public static final java.lang.String MODIFY_MSIM_PHONE_STATE = "cyanogenmod.permission.MODIFY_MSIM_PHONE_STATE"; field public static final java.lang.String MODIFY_NETWORK_SETTINGS = "cyanogenmod.permission.MODIFY_NETWORK_SETTINGS"; field public static final java.lang.String MODIFY_PROFILES = "cyanogenmod.permission.MODIFY_PROFILES"; @@ -858,6 +858,33 @@ package cyanogenmod.preference { method protected boolean persistBoolean(boolean); } + public class RemotePreference extends cyanogenmod.preference.SelfRemovingPreference { + ctor public RemotePreference(android.content.Context, android.util.AttributeSet, int, int); + ctor public RemotePreference(android.content.Context, android.util.AttributeSet, int); + ctor public RemotePreference(android.content.Context, android.util.AttributeSet); + method public android.content.Intent getReceiverIntent(); + method protected java.lang.String getRemoteKey(android.os.Bundle); + method public void onAttached(); + method public void onDetached(); + method public void onRemoteUpdated(android.os.Bundle); + field public static final java.lang.String ACTION_REFRESH_PREFERENCE = "cyanogenmod.intent.action.REFRESH_PREFERENCE"; + field public static final java.lang.String ACTION_UPDATE_PREFERENCE = "cyanogenmod.intent.action.UPDATE_PREFERENCE"; + field public static final java.lang.String EXTRA_ENABLED = ":cm:pref_enabled"; + field public static final java.lang.String EXTRA_KEY = ":cm:pref_key"; + field public static final java.lang.String EXTRA_SUMMARY = ":cm:pref_summary"; + field public static final java.lang.String META_REMOTE_KEY = "org.cyanogenmod.settings.summary.key"; + field public static final java.lang.String META_REMOTE_RECEIVER = "org.cyanogenmod.settings.summary.receiver"; + field protected final android.content.Context mContext; + } + + public class RemotePreferenceUpdater extends android.content.BroadcastReceiver { + ctor public RemotePreferenceUpdater(); + method protected boolean fillResultExtras(android.content.Context, java.lang.String, android.os.Bundle); + method protected java.lang.String getSummary(android.content.Context, java.lang.String); + method public static void notifyChanged(android.content.Context, java.lang.String); + method public void onReceive(android.content.Context, android.content.Intent); + } + public class SecureSettingSwitchPreference extends cyanogenmod.preference.SelfRemovingSwitchPreference { ctor public SecureSettingSwitchPreference(android.content.Context, android.util.AttributeSet, int); ctor public SecureSettingSwitchPreference(android.content.Context, android.util.AttributeSet); diff --git a/cm/res/AndroidManifest.xml b/cm/res/AndroidManifest.xml index 04c11030..115cc499 100644 --- a/cm/res/AndroidManifest.xml +++ b/cm/res/AndroidManifest.xml @@ -29,8 +29,8 @@ - - + + @@ -279,11 +279,10 @@ android:description="@string/permdesc_dataUsageWrite" android:protectionLevel="signature|privileged" /> - - + - diff --git a/cm/res/res/values/strings.xml b/cm/res/res/values/strings.xml index 644f70e7..b9ed9c81 100644 --- a/cm/res/res/values/strings.xml +++ b/cm/res/res/values/strings.xml @@ -237,8 +237,8 @@ Privacy Guard - - manage remote settings - Allows an app to manage CyanogenMod settings + + manage remote settings + Allows an app to manage remote settings diff --git a/sdk/src/java/cyanogenmod/preference/RemotePreference.java b/sdk/src/java/cyanogenmod/preference/RemotePreference.java new file mode 100644 index 00000000..42c50c85 --- /dev/null +++ b/sdk/src/java/cyanogenmod/preference/RemotePreference.java @@ -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 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; + } +} diff --git a/sdk/src/java/cyanogenmod/preference/RemotePreferenceManager.java b/sdk/src/java/cyanogenmod/preference/RemotePreferenceManager.java new file mode 100644 index 00000000..83133605 --- /dev/null +++ b/sdk/src/java/cyanogenmod/preference/RemotePreferenceManager.java @@ -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 mCache = new ArrayMap<>(); + private final Map 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); + } + } + } + }); + } + } + } + } + }; +} diff --git a/sdk/src/java/cyanogenmod/preference/RemotePreferenceUpdater.java b/sdk/src/java/cyanogenmod/preference/RemotePreferenceUpdater.java new file mode 100644 index 00000000..aa2ea622 --- /dev/null +++ b/sdk/src/java/cyanogenmod/preference/RemotePreferenceUpdater.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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); + } +} diff --git a/sdk/src/java/org/cyanogenmod/internal/cmparts/PartsList.java b/sdk/src/java/org/cyanogenmod/internal/cmparts/PartsList.java index 0097efc3..7334dca6 100644 --- a/sdk/src/java/org/cyanogenmod/internal/cmparts/PartsList.java +++ b/sdk/src/java/org/cyanogenmod/internal/cmparts/PartsList.java @@ -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 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) {