Notification listener backup & restore
We now back up & restore the set of enabled notification listeners. Post-
restore, a listener that had been enabled on the ancestral device will be
enabled on the current device as soon as it's installed, matching the
user's previous configuration. After this has happened the enable/disable
state for that app is not "sticky"; disabling it again will work as
expected.
The infrastructure for accomplishing this is general: it can be leveraged
by any ManagedServices derivative. There's a bit of extra wiring in the
settings provider to support the restore-time information flow as well.
This is because ManagedServices -- like many other parts of the system --
monitors writes to the settings provider and does work in response to new
writes of the elements that it cares about. Unfortunately this means that
there is no way to use the BackupAgent's restoreFinished() hook to post-
process the restored data: by the time it is run, the ManagedService's
observers have already executed and culled any unknown components from
the description that was just pushed into settings.
As of this patch, the settings provider's restore logic knows that a
particular settings element will require a message to interested observers
about the restore-driven change. The message is delivered as a broadcast,
and is sent after the new value has been committed to the settings db.
Adding other system ManagedService handling that parallels this will only
require adding a new corresponding entry to the table of individual settings
for which the relevant "this settings element is being restored" broadcast
is sent, found in SettingsHelper.
(It isn't sent for all settings elements because very few settings elements
have semantics that require it; 3rd party code won't be running yet during
platform restore anyway; and sending up to hundreds of broadcasts during
setup & restore is far from ideal.)
Bug 19254153
Change-Id: Ib8268c6cb273862a3ee089d2764f3bff4a299103
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 2ed8c44..f685475 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2790,6 +2790,31 @@
/** {@hide} */
public static final String ACTION_MASTER_CLEAR = "android.intent.action.MASTER_CLEAR";
+ /**
+ * Broadcast action: report that a settings element is being restored from backup. The intent
+ * contains three extras: EXTRA_SETTING_NAME is a string naming the restored setting,
+ * EXTRA_SETTING_NEW_VALUE is the value being restored, and EXTRA_SETTING_PREVIOUS_VALUE
+ * is the value of that settings entry prior to the restore operation. All of these values are
+ * represented as strings.
+ *
+ * <p>This broadcast is sent only for settings provider entries known to require special handling
+ * around restore time. These entries are found in the BROADCAST_ON_RESTORE table within
+ * the provider's backup agent implementation.
+ *
+ * @see #EXTRA_SETTING_NAME
+ * @see #EXTRA_SETTING_PREVIOUS_VALUE
+ * @see #EXTRA_SETTING_NEW_VALUE
+ * {@hide}
+ */
+ public static final String ACTION_SETTING_RESTORED = "android.os.action.SETTING_RESTORED";
+
+ /** {@hide} */
+ public static final String EXTRA_SETTING_NAME = "setting_name";
+ /** {@hide} */
+ public static final String EXTRA_SETTING_PREVIOUS_VALUE = "previous_value";
+ /** {@hide} */
+ public static final String EXTRA_SETTING_NEW_VALUE = "new_value";
+
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Standard intent categories (see addCategory()).
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4c452aa..bb0959e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5374,6 +5374,7 @@
ACCESSIBILITY_SCRIPT_INJECTION,
BACKUP_AUTO_RESTORE,
ENABLED_ACCESSIBILITY_SERVICES,
+ ENABLED_NOTIFICATION_LISTENERS,
TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
TOUCH_EXPLORATION_ENABLED,
ACCESSIBILITY_ENABLED,
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index ed4776b..4d6b5f6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -90,6 +90,8 @@
<protected-broadcast android:name="android.appwidget.action.APPWIDGET_HOST_RESTORED" />
<protected-broadcast android:name="android.appwidget.action.APPWIDGET_RESTORED" />
+ <protected-broadcast android:name="android.os.action.SETTING_RESTORED" />
+
<protected-broadcast android:name="android.backup.intent.RUN" />
<protected-broadcast android:name="android.backup.intent.CLEAR" />
<protected-broadcast android:name="android.backup.intent.INIT" />
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index eac83d8..7f826ef 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -793,7 +793,7 @@
}
// Figure out the white list and redirects to the global table.
- String[] whitelist = null;
+ final String[] whitelist;
if (contentUri.equals(Settings.Secure.CONTENT_URI)) {
whitelist = Settings.Secure.SETTINGS_TO_BACKUP;
} else if (contentUri.equals(Settings.System.CONTENT_URI)) {
@@ -809,6 +809,7 @@
Map<String, String> cachedEntries = new HashMap<String, String>();
ContentValues contentValues = new ContentValues(2);
SettingsHelper settingsHelper = mSettingsHelper;
+ ContentResolver cr = getContentResolver();
final int whiteListSize = whitelist.length;
for (int i = 0; i < whiteListSize; i++) {
@@ -841,14 +842,7 @@
final Uri destination = (movedToGlobal != null && movedToGlobal.contains(key))
? Settings.Global.CONTENT_URI
: contentUri;
-
- // The helper doesn't care what namespace the keys are in
- if (settingsHelper.restoreValue(key, value)) {
- contentValues.clear();
- contentValues.put(Settings.NameValueTable.NAME, key);
- contentValues.put(Settings.NameValueTable.VALUE, value);
- getContentResolver().insert(destination, contentValues);
- }
+ settingsHelper.restoreValue(this, cr, contentValues, destination, key, value);
if (DEBUG) {
Log.d(TAG, "Restored setting: " + destination + " : "+ key + "=" + value);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 4144c80..1cad610 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -19,7 +19,10 @@
import android.app.ActivityManagerNative;
import android.app.IActivityManager;
import android.app.backup.IBackupManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Configuration;
import android.location.LocationManager;
import android.media.AudioManager;
@@ -28,10 +31,12 @@
import android.os.IPowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.ArraySet;
import java.util.Locale;
@@ -41,6 +46,48 @@
private AudioManager mAudioManager;
private TelephonyManager mTelephonyManager;
+ /**
+ * A few settings elements are special in that a restore of those values needs to
+ * be post-processed by relevant parts of the OS. A restore of any settings element
+ * mentioned in this table will therefore cause the system to send a broadcast with
+ * the {@link Intent#ACTION_SETTING_RESTORED} action, with extras naming the
+ * affected setting and supplying its pre-restore value for comparison.
+ *
+ * @see Intent#ACTION_SETTING_RESTORED
+ * @see System#SETTINGS_TO_BACKUP
+ * @see Secure#SETTINGS_TO_BACKUP
+ * @see Global#SETTINGS_TO_BACKUP
+ *
+ * {@hide}
+ */
+ private static final ArraySet<String> sBroadcastOnRestore;
+ static {
+ sBroadcastOnRestore = new ArraySet<String>(1);
+ sBroadcastOnRestore.add(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
+ }
+
+ private interface SettingsLookup {
+ public String lookup(ContentResolver resolver, String name, int userHandle);
+ }
+
+ private static SettingsLookup sSystemLookup = new SettingsLookup() {
+ public String lookup(ContentResolver resolver, String name, int userHandle) {
+ return Settings.System.getStringForUser(resolver, name, userHandle);
+ }
+ };
+
+ private static SettingsLookup sSecureLookup = new SettingsLookup() {
+ public String lookup(ContentResolver resolver, String name, int userHandle) {
+ return Settings.Secure.getStringForUser(resolver, name, userHandle);
+ }
+ };
+
+ private static SettingsLookup sGlobalLookup = new SettingsLookup() {
+ public String lookup(ContentResolver resolver, String name, int userHandle) {
+ return Settings.Global.getStringForUser(resolver, name, userHandle);
+ }
+ };
+
public SettingsHelper(Context context) {
mContext = context;
mAudioManager = (AudioManager) context
@@ -58,24 +105,67 @@
* some cases the data will be written by the call to the appropriate API,
* and in some cases the property value needs to be modified before setting.
*/
- public boolean restoreValue(String name, String value) {
- if (Settings.System.SCREEN_BRIGHTNESS.equals(name)) {
- setBrightness(Integer.parseInt(value));
- } else if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
- setSoundEffects(Integer.parseInt(value) == 1);
- } else if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
- setGpsLocation(value);
- return false;
- } else if (Settings.Secure.BACKUP_AUTO_RESTORE.equals(name)) {
- setAutoRestore(Integer.parseInt(value) == 1);
- } else if (isAlreadyConfiguredCriticalAccessibilitySetting(name)) {
- return false;
- } else if (Settings.System.RINGTONE.equals(name)
- || Settings.System.NOTIFICATION_SOUND.equals(name)) {
- setRingtone(name, value);
- return false;
+ public void restoreValue(Context context, ContentResolver cr, ContentValues contentValues,
+ Uri destination, String name, String value) {
+ // Will we need a post-restore broadcast for this element?
+ String oldValue = null;
+ boolean sendBroadcast = false;
+ final SettingsLookup table;
+
+ if (destination.equals(Settings.Secure.CONTENT_URI)) {
+ table = sSecureLookup;
+ } else if (destination.equals(Settings.System.CONTENT_URI)) {
+ table = sSystemLookup;
+ } else { /* must be GLOBAL; this was preflighted by the caller */
+ table = sGlobalLookup;
}
- return true;
+
+ if (sBroadcastOnRestore.contains(name)) {
+ oldValue = table.lookup(cr, name, UserHandle.USER_OWNER);
+ sendBroadcast = true;
+ }
+
+ try {
+ if (Settings.System.SCREEN_BRIGHTNESS.equals(name)) {
+ setBrightness(Integer.parseInt(value));
+ // fall through to the ordinary write to settings
+ } else if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
+ setSoundEffects(Integer.parseInt(value) == 1);
+ // fall through to the ordinary write to settings
+ } else if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
+ setGpsLocation(value);
+ return;
+ } else if (Settings.Secure.BACKUP_AUTO_RESTORE.equals(name)) {
+ setAutoRestore(Integer.parseInt(value) == 1);
+ } else if (isAlreadyConfiguredCriticalAccessibilitySetting(name)) {
+ return;
+ } else if (Settings.System.RINGTONE.equals(name)
+ || Settings.System.NOTIFICATION_SOUND.equals(name)) {
+ setRingtone(name, value);
+ return;
+ }
+
+ // Default case: write the restored value to settings
+ contentValues.clear();
+ contentValues.put(Settings.NameValueTable.NAME, name);
+ contentValues.put(Settings.NameValueTable.VALUE, value);
+ cr.insert(destination, contentValues);
+ } catch (Exception e) {
+ // If we fail to apply the setting, by definition nothing happened
+ sendBroadcast = false;
+ } finally {
+ // If this was an element of interest, send the "we just restored it"
+ // broadcast with the historical value now that the new value has
+ // been committed and observers kicked off.
+ if (sendBroadcast) {
+ Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+ .setPackage("android").addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+ .putExtra(Intent.EXTRA_SETTING_NAME, name)
+ .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, value)
+ .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, oldValue);
+ context.sendBroadcastAsUser(intent, UserHandle.OWNER, null);
+ }
+ }
}
public String onBackupValue(String name, String value) {
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 0c7d71b..9ccb2ea 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -18,10 +18,12 @@
import android.app.ActivityManager;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -51,6 +53,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
/**
@@ -74,6 +77,7 @@
private final UserProfiles mUserProfiles;
private final SettingsObserver mSettingsObserver;
private final Config mConfig;
+ private ArraySet<String> mRestored;
// contains connections to all connected services, including app services
// and system services
@@ -91,6 +95,8 @@
// user change).
private int[] mLastSeenProfileIds;
+ private final BroadcastReceiver mRestoreReceiver;
+
public ManagedServices(Context context, Handler handler, Object mutex,
UserProfiles userProfiles) {
mContext = context;
@@ -98,6 +104,24 @@
mUserProfiles = userProfiles;
mConfig = getConfig();
mSettingsObserver = new SettingsObserver(handler);
+
+ mRestoreReceiver = new SettingRestoredReceiver();
+ IntentFilter filter = new IntentFilter(Intent.ACTION_SETTING_RESTORED);
+ context.registerReceiver(mRestoreReceiver, filter);
+ }
+
+ class SettingRestoredReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_SETTING_RESTORED.equals(intent.getAction())) {
+ String element = intent.getStringExtra(Intent.EXTRA_SETTING_NAME);
+ if (Objects.equals(element, mConfig.secureSettingName)) {
+ String prevValue = intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE);
+ String newValue = intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE);
+ settingRestored(element, prevValue, newValue, getSendingUserId());
+ }
+ }
+ }
}
abstract protected Config getConfig();
@@ -140,6 +164,31 @@
}
}
+ // By convention, restored settings are replicated to another settings
+ // entry, named similarly but with a disambiguation suffix.
+ public static final String restoredSettingName(Config config) {
+ return config.secureSettingName + ":restored";
+ }
+
+ // The OS has done a restore of this service's saved state. We clone it to the
+ // 'restored' reserve, and then once we return and the actual write to settings is
+ // performed, our observer will do the work of maintaining the restored vs live
+ // settings data.
+ public void settingRestored(String element, String oldValue, String newValue, int userid) {
+ if (DEBUG) Slog.d(TAG, "Restored managed service setting: " + element
+ + " ovalue=" + oldValue + " nvalue=" + newValue);
+ if (mConfig.secureSettingName.equals(element)) {
+ if (element != null) {
+ mRestored = null;
+ Settings.Secure.putStringForUser(mContext.getContentResolver(),
+ restoredSettingName(mConfig),
+ newValue,
+ userid);
+ disableNonexistentServices(userid);
+ }
+ }
+ }
+
public void onPackagesChanged(boolean queryReplace, String[] pkgList) {
if (DEBUG) Slog.d(TAG, "onPackagesChanged queryReplace=" + queryReplace
+ " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList))
@@ -211,8 +260,23 @@
}
private void disableNonexistentServices(int userId) {
+ final ContentResolver cr = mContext.getContentResolver();
+ boolean restoredChanged = false;
+ if (mRestored == null) {
+ String restoredSetting = Settings.Secure.getStringForUser(
+ cr,
+ restoredSettingName(mConfig),
+ userId);
+ if (!TextUtils.isEmpty(restoredSetting)) {
+ if (DEBUG) Slog.d(TAG, "restored: " + restoredSetting);
+ String[] restored = restoredSetting.split(ENABLED_SERVICES_SEPARATOR);
+ mRestored = new ArraySet<String>(Arrays.asList(restored));
+ } else {
+ mRestored = new ArraySet<String>();
+ }
+ }
String flatIn = Settings.Secure.getStringForUser(
- mContext.getContentResolver(),
+ cr,
mConfig.secureSettingName,
userId);
if (!TextUtils.isEmpty(flatIn)) {
@@ -228,14 +292,16 @@
ResolveInfo resolveInfo = installedServices.get(i);
ServiceInfo info = resolveInfo.serviceInfo;
+ ComponentName component = new ComponentName(info.packageName, info.name);
if (!mConfig.bindPermission.equals(info.permission)) {
Slog.w(TAG, "Skipping " + getCaption() + " service "
+ info.packageName + "/" + info.name
+ ": it does not require the permission "
+ mConfig.bindPermission);
+ restoredChanged |= mRestored.remove(component.flattenToString());
continue;
}
- installed.add(new ComponentName(info.packageName, info.name));
+ installed.add(component);
}
String flatOut = "";
@@ -246,16 +312,27 @@
ComponentName enabledComponent = ComponentName.unflattenFromString(enabled[i]);
if (installed.contains(enabledComponent)) {
remaining.add(enabled[i]);
+ restoredChanged |= mRestored.remove(enabled[i]);
}
}
+ remaining.addAll(mRestored);
flatOut = TextUtils.join(ENABLED_SERVICES_SEPARATOR, remaining);
}
if (DEBUG) Slog.v(TAG, "flat after: " + flatOut);
if (!flatIn.equals(flatOut)) {
- Settings.Secure.putStringForUser(mContext.getContentResolver(),
+ Settings.Secure.putStringForUser(cr,
mConfig.secureSettingName,
flatOut, userId);
}
+ if (restoredChanged) {
+ if (DEBUG) Slog.d(TAG, "restored changed; rewriting");
+ final String flatRestored = TextUtils.join(ENABLED_SERVICES_SEPARATOR,
+ mRestored.toArray());
+ Settings.Secure.putStringForUser(cr,
+ restoredSettingName(mConfig),
+ flatRestored,
+ userId);
+ }
}
}