Handle races caused by rapid settings changed broadcasts

- Fix b/10447517

Change-Id: I63ef98c9023cee1a15be61b966aed06dc35e9bd5
This commit is contained in:
Tom O'Neill
2013-08-23 15:23:12 -07:00
parent 32e016b5fa
commit e17ce5fb73
3 changed files with 232 additions and 114 deletions

View File

@@ -17,12 +17,17 @@
package com.android.settings.location; package com.android.settings.location;
import android.content.Intent; import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.Immutable;
import com.android.internal.util.Preconditions;
/** /**
* Specifies a setting that is being injected into Settings > Location > Location services. * Specifies a setting that is being injected into Settings > Location > Location services.
* *
* @see android.location.SettingInjectorService * @see android.location.SettingInjectorService
*/ */
@Immutable
class InjectedSetting { class InjectedSetting {
/** /**
@@ -53,13 +58,30 @@ class InjectedSetting {
*/ */
public final String settingsActivity; public final String settingsActivity;
public InjectedSetting(String packageName, String className, private InjectedSetting(String packageName, String className,
String title, int iconId, String settingsActivity) { String title, int iconId, String settingsActivity) {
this.packageName = packageName; this.packageName = Preconditions.checkNotNull(packageName, "packageName");
this.className = className; this.className = Preconditions.checkNotNull(className, "className");
this.title = title; this.title = Preconditions.checkNotNull(title, "title");
this.iconId = iconId; this.iconId = iconId;
this.settingsActivity = settingsActivity; this.settingsActivity = Preconditions.checkNotNull(settingsActivity);
}
/**
* Returns a new instance, or null.
*/
public static InjectedSetting newInstance(String packageName, String className,
String title, int iconId, String settingsActivity) {
if (packageName == null || className == null ||
TextUtils.isEmpty(title) || TextUtils.isEmpty(settingsActivity)) {
if (Log.isLoggable(SettingsInjector.TAG, Log.WARN)) {
Log.w(SettingsInjector.TAG, "Illegal setting specification: package="
+ packageName + ", class=" + className
+ ", title=" + title + ", settingsActivity=" + settingsActivity);
}
return null;
}
return new InjectedSetting(packageName, className, title, iconId, settingsActivity);
} }
@Override @Override
@@ -81,4 +103,26 @@ class InjectedSetting {
intent.setClassName(packageName, className); intent.setClassName(packageName, className);
return intent; return intent;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof InjectedSetting)) return false;
InjectedSetting that = (InjectedSetting) o;
return packageName.equals(that.packageName) && className.equals(that.className)
&& title.equals(that.title) && iconId == that.iconId
&& settingsActivity.equals(that.settingsActivity);
}
@Override
public int hashCode() {
int result = packageName.hashCode();
result = 31 * result + className.hashCode();
result = 31 * result + title.hashCode();
result = 31 * result + iconId;
result = 31 * result + settingsActivity.hashCode();
return result;
}
} }

View File

@@ -31,6 +31,7 @@ import android.preference.PreferenceGroup;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.widget.CompoundButton; import android.widget.CompoundButton;
import android.widget.Switch; import android.widget.Switch;
@@ -47,6 +48,9 @@ import java.util.List;
*/ */
public class LocationSettings extends LocationSettingsBase public class LocationSettings extends LocationSettingsBase
implements CompoundButton.OnCheckedChangeListener { implements CompoundButton.OnCheckedChangeListener {
private static final String TAG = "LocationSettings";
/** Key for preference screen "Mode" */ /** Key for preference screen "Mode" */
private static final String KEY_LOCATION_MODE = "location_mode"; private static final String KEY_LOCATION_MODE = "location_mode";
/** Key for preference category "Recent location requests" */ /** Key for preference category "Recent location requests" */
@@ -146,8 +150,6 @@ public class LocationSettings extends LocationSettingsBase
} }
}); });
final PreferenceManager preferenceManager = getPreferenceManager();
PreferenceCategory categoryRecentLocationRequests = PreferenceCategory categoryRecentLocationRequests =
(PreferenceCategory) root.findPreference(KEY_RECENT_LOCATION_REQUESTS); (PreferenceCategory) root.findPreference(KEY_RECENT_LOCATION_REQUESTS);
RecentLocationApps recentApps = new RecentLocationApps(activity, mStatsHelper); RecentLocationApps recentApps = new RecentLocationApps(activity, mStatsHelper);
@@ -172,6 +174,9 @@ public class LocationSettings extends LocationSettingsBase
mReceiver = new BroadcastReceiver() { mReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Received settings change intent: " + intent);
}
injector.reloadStatusMessages(); injector.reloadStatusMessages();
} }
}; };

View File

@@ -31,18 +31,20 @@ import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.Messenger; import android.os.Messenger;
import android.preference.Preference; import android.preference.Preference;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
import android.util.Xml; import android.util.Xml;
import com.android.settings.R;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import com.android.settings.R;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group.
@@ -59,7 +61,7 @@ import java.util.List;
* {@link SettingInjectorService#UPDATE_INTENT}. * {@link SettingInjectorService#UPDATE_INTENT}.
*/ */
class SettingsInjector { class SettingsInjector {
private static final String TAG = "SettingsInjector"; static final String TAG = "SettingsInjector";
private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000; private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000;
@@ -79,11 +81,35 @@ class SettingsInjector {
*/ */
public static final String ATTRIBUTES_NAME = "injected-location-setting"; public static final String ATTRIBUTES_NAME = "injected-location-setting";
private Context mContext; /**
private StatusLoader mLoader = null; * {@link Message#what} value for starting to load status values
* in case we aren't already in the process of loading them.
*/
private static final int WHAT_RELOAD = 1;
/**
* {@link Message#what} value sent after receiving a status message.
*/
private static final int WHAT_RECEIVED_STATUS = 2;
/**
* {@link Message#what} value sent after the timeout waiting for a status message.
*/
private static final int WHAT_TIMEOUT = 3;
private final Context mContext;
/**
* The settings that were injected
*/
private final Set<Setting> mSettings;
private final Handler mHandler;
public SettingsInjector(Context context) { public SettingsInjector(Context context) {
mContext = context; mContext = context;
mSettings = new HashSet<Setting>();
mHandler = new StatusLoadingHandler();
} }
/** /**
@@ -168,6 +194,9 @@ class SettingsInjector {
} }
} }
/**
* Returns an immutable representation of the static attributes for the setting, or null.
*/
private static InjectedSetting parseAttributes( private static InjectedSetting parseAttributes(
String packageName, String className, Resources res, AttributeSet attrs) { String packageName, String className, Resources res, AttributeSet attrs) {
@@ -175,86 +204,22 @@ class SettingsInjector {
try { try {
// Note that to help guard against malicious string injection, we do not allow dynamic // Note that to help guard against malicious string injection, we do not allow dynamic
// specification of the label (setting title) // specification of the label (setting title)
final int labelId = sa.getResourceId(
android.R.styleable.InjectedLocationSetting_label, 0);
final String label = sa.getString(android.R.styleable.InjectedLocationSetting_label); final String label = sa.getString(android.R.styleable.InjectedLocationSetting_label);
final int iconId = sa.getResourceId( final int iconId = sa.getResourceId(
android.R.styleable.InjectedLocationSetting_icon, 0); android.R.styleable.InjectedLocationSetting_icon, 0);
final String settingsActivity = final String settingsActivity =
sa.getString(android.R.styleable.InjectedLocationSetting_settingsActivity); sa.getString(android.R.styleable.InjectedLocationSetting_settingsActivity);
if (Log.isLoggable(TAG, Log.DEBUG)) { if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "parsed labelId: " + labelId + ", label: " + label Log.d(TAG, "parsed label: " + label + ", iconId: " + iconId
+ ", iconId: " + iconId); + ", settingsActivity: " + settingsActivity);
} }
if (labelId == 0 || TextUtils.isEmpty(label) || TextUtils.isEmpty(settingsActivity)) { return InjectedSetting.newInstance(packageName, className,
return null;
}
return new InjectedSetting(packageName, className,
label, iconId, settingsActivity); label, iconId, settingsActivity);
} finally { } finally {
sa.recycle(); sa.recycle();
} }
} }
private final class StatusLoader {
private final Intent mIntent;
private final StatusLoader mPrev;
private boolean mLoaded = false;
/**
* Creates a loader and chains with the previous loader.
*/
public StatusLoader(Intent intent, StatusLoader prev) {
mIntent = intent;
mPrev = prev;
}
/**
* Clears the loaded flag for the whole chain.
*/
private void setNotLoaded() {
mLoaded = false;
if (mPrev != null) {
mPrev.setNotLoaded();
}
}
/**
* Reloads the whole chain.
*/
public void reload() {
setNotLoaded();
loadIfNotLoaded();
}
/**
* If the current message hasn't been loaded, loads the status messages
* and set time out for the next message.
*/
public void loadIfNotLoaded() {
if (mLoaded) {
return;
}
mContext.startService(mIntent);
if (mPrev != null) {
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// Continue with the next item in the chain.
mPrev.loadIfNotLoaded();
}
};
// Ensure that we start loading the previous setting in the chain if the current
// setting hasn't loaded before the timeout
handler.sendMessageDelayed(
Message.obtain(handler), INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
}
mLoaded = true;
}
}
/** /**
* Gets a list of preferences that other apps have injected. * Gets a list of preferences that other apps have injected.
* *
@@ -264,17 +229,12 @@ class SettingsInjector {
public List<Preference> getInjectedSettings() { public List<Preference> getInjectedSettings() {
Iterable<InjectedSetting> settings = getSettings(); Iterable<InjectedSetting> settings = getSettings();
ArrayList<Preference> prefs = new ArrayList<Preference>(); ArrayList<Preference> prefs = new ArrayList<Preference>();
mLoader = null;
for (InjectedSetting setting : settings) { for (InjectedSetting setting : settings) {
Preference pref = addServiceSetting(prefs, setting); Preference pref = addServiceSetting(prefs, setting);
Intent intent = createUpdatingIntent(pref, setting, mLoader); mSettings.add(new Setting(setting, pref));
mLoader = new StatusLoader(intent, mLoader);
} }
// Start a thread to load each list item status. reloadStatusMessages();
if (mLoader != null) {
mLoader.loadIfNotLoaded();
}
return prefs; return prefs;
} }
@@ -283,9 +243,10 @@ class SettingsInjector {
* Reloads the status messages for all the preference items. * Reloads the status messages for all the preference items.
*/ */
public void reloadStatusMessages() { public void reloadStatusMessages() {
if (mLoader != null) { if (Log.isLoggable(TAG, Log.DEBUG)) {
mLoader.reload(); Log.d(TAG, "reloadingStatusMessages: " + mSettings);
} }
mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD));
} }
/** /**
@@ -308,33 +269,141 @@ class SettingsInjector {
} }
/** /**
* Creates an Intent to ask the receiver for the current status for the setting, and display it * Loads the setting status values one at a time. Each load starts a subclass of {@link
* when it replies. * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
* once.
*/ */
private static Intent createUpdatingIntent( private final class StatusLoadingHandler extends Handler {
final Preference pref, final InjectedSetting info, final StatusLoader prev) {
final Intent receiverIntent = info.getServiceIntent(); /**
Handler handler = new Handler() { * Settings whose status values need to be loaded. A set is used to prevent redundant loads
@Override * even if {@link #reloadStatusMessages()} is called many times in rapid succession (for
public void handleMessage(Message msg) { * example, if we receive a lot of
Bundle bundle = msg.getData(); * {@link android.location.SettingInjectorService#UPDATE_INTENT} broadcasts).
String status = bundle.getString(SettingInjectorService.STATUS_KEY); * <p/>
boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true); * We use a linked hash set to ensure that when {@link #reloadStatusMessages()} is called,
if (Log.isLoggable(TAG, Log.DEBUG)) { * any settings that haven't been loaded yet will finish loading before any already-loaded
Log.d(TAG, info + ": received " + msg + ", bundle: " + bundle); * messages are loaded again.
} */
pref.setSummary(status); private LinkedHashSet<Setting> mSettingsToLoad = new LinkedHashSet<Setting>();
pref.setEnabled(enabled);
if (prev != null) { /**
prev.loadIfNotLoaded(); * Whether we're in the middle of loading settings.
} */
private boolean mLoading;
@Override
public void handleMessage(Message msg) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "handleMessage start: " + msg + ", mSettingsToLoad: " + mSettingsToLoad);
}
switch (msg.what) {
case WHAT_RELOAD:
mSettingsToLoad.addAll(mSettings);
if (mLoading) {
// Already waiting for a service to return its status, don't ask a new one
return;
}
mLoading = true;
break;
case WHAT_TIMEOUT:
if (Log.isLoggable(TAG, Log.WARN)) {
final Setting setting = (Setting) msg.obj;
setting.timedOut = true;
Log.w(TAG, "Timed out trying to get status for: " + setting);
}
break;
case WHAT_RECEIVED_STATUS:
final Setting setting = (Setting) msg.obj;
if (setting.timedOut) {
// We've already restarted retrieving the next setting, don't start another
return;
}
// Received the setting without timeout, clear any previous timed out status
setting.timedOut = false;
break;
default:
throw new IllegalArgumentException("Unexpected what: " + msg);
}
// Remove the next setting to load from the queue, if any
Iterator<Setting> iter = mSettingsToLoad.iterator();
if (!iter.hasNext()) {
mLoading = false;
return;
}
Setting setting = iter.next();
iter.remove();
// Request the status value
Intent intent = setting.createUpdatingIntent();
mContext.startService(intent);
// Ensure that if receiving the status value takes too long, we start loading the
// next value anyway
Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
removeMessages(WHAT_TIMEOUT);
sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "handleMessage end: " + msg + ", mSettingsToLoad: " + mSettingsToLoad);
} }
};
Messenger messenger = new Messenger(handler);
receiverIntent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, info + ": sending rcv-intent: " + receiverIntent + ", handler: " + handler);
} }
return receiverIntent; }
/**
* Represents an injected setting and the corresponding preference.
*/
private final class Setting {
public final InjectedSetting setting;
public final Preference preference;
public boolean timedOut = false;
private Setting(InjectedSetting setting, Preference preference) {
this.setting = setting;
this.preference = preference;
}
@Override
public String toString() {
return "Setting{" +
"setting=" + setting +
", preference=" + preference +
", timedOut=" + timedOut +
'}';
}
/**
* Creates an Intent to ask the receiver for the current status for the setting, and display
* it when it replies.
*/
public Intent createUpdatingIntent() {
final Intent receiverIntent = setting.getServiceIntent();
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Bundle bundle = msg.getData();
String status = bundle.getString(SettingInjectorService.STATUS_KEY);
boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
}
preference.setSummary(status);
preference.setEnabled(enabled);
mHandler.sendMessage(
mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
}
};
Messenger messenger = new Messenger(handler);
receiverIntent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, setting + ": sending rcv-intent: " + receiverIntent
+ ", handler: " + handler);
}
return receiverIntent;
}
} }
} }