Loading res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -2380,6 +2380,9 @@ <string name="location_mode_battery_saving_description">Use Wi\u2011Fi and mobile networks to estimate location</string> <!-- [CHAR LIMIT=130] Location mode screen, description for sensors only mode --> <string name="location_mode_sensors_only_description">Use GPS to pinpoint your location</string> <!-- [CHAR LIMIT=130] Location mode screen, temporary value to show as the status of a location setting injected by an external app while the app is being queried for the actual value --> <string name="location_loading_injected_setting">Retrieving…</string> <!-- [CHAR LIMIT=30] Security & location settings screen, setting check box label for Google location service (cell ID, wifi, etc.) --> <string name="location_network_based">Wi\u2011Fi & mobile network location</string> Loading src/com/android/settings/location/InjectedSetting.java 0 → 100644 +84 −0 Original line number Diff line number Diff line /* * Copyright (C) 2013 The Android Open Source 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 com.android.settings.location; import android.content.Intent; /** * Specifies a setting that is being injected into Settings > Location > Location services. * * @see android.location.SettingInjectorService */ class InjectedSetting { /** * Package for the subclass of {@link android.location.SettingInjectorService} and for the * settings activity. */ public final String packageName; /** * Class name for the subclass of {@link android.location.SettingInjectorService} that * specifies dynamic values for the location setting. */ public final String className; /** * The {@link android.preference.Preference#getTitle()} value. */ public final String title; /** * The {@link android.preference.Preference#getIcon()} value. */ public final int iconId; /** * The activity to launch to allow the user to modify the settings value. Assumed to be in the * {@link #packageName} package. */ public final String settingsActivity; public InjectedSetting(String packageName, String className, String title, int iconId, String settingsActivity) { this.packageName = packageName; this.className = className; this.title = title; this.iconId = iconId; this.settingsActivity = settingsActivity; } @Override public String toString() { return "InjectedSetting{" + "mPackageName='" + packageName + '\'' + ", mClassName='" + className + '\'' + ", label=" + title + ", iconId=" + iconId + ", settingsActivity='" + settingsActivity + '\'' + '}'; } /** * Returns the intent to start the {@link #className} service. */ public Intent getServiceIntent() { Intent intent = new Intent(); intent.setClassName(packageName, className); return intent; } } src/com/android/settings/location/LocationSettings.java +2 −0 Original line number Diff line number Diff line Loading @@ -106,6 +106,8 @@ public class LocationSettings extends LocationSettingsBase RecentLocationApps recentApps = new RecentLocationApps(activity); recentApps.fillAppList(mRecentLocationRequests); SettingsInjector.addInjectedSettings(mLocationServices, activity, getPreferenceManager()); if (activity instanceof PreferenceActivity) { PreferenceActivity preferenceActivity = (PreferenceActivity) activity; // Only show the master switch when we're not in multi-pane mode, and not being used as Loading src/com/android/settings/location/SettingsInjector.java 0 → 100644 +266 −0 Original line number Diff line number Diff line /* * Copyright (C) 2013 The Android Open Source 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 com.android.settings.location; import android.R; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.location.SettingInjectorService; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. * * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that * class directly because it is not a good match for our use case: we do not need the caching, and * so do not want the additional resource hit at app install/upgrade time; and we would have to * suppress the tie-breaking between multiple services reporting settings with the same name. * Code-sharing would require extracting {@link * android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources, * String, android.util.AttributeSet)} into an interface, which didn't seem worth it. */ class SettingsInjector { private static final String TAG = "SettingsInjector"; /** * Intent action marking the receiver as injecting a setting */ public static final String RECEIVER_INTENT = "com.android.settings.InjectedLocationSetting"; /** * Name of the meta-data tag used to specify the resource file that includes the settings * attributes. */ public static final String META_DATA_NAME = "com.android.settings.InjectedLocationSetting"; /** * Name of the XML tag that includes the attributes for the setting. */ public static final String ATTRIBUTES_NAME = "injected-location-setting"; /** * Intent action a client should broadcast when the value of one of its injected settings has * changed, so that the setting can be updated in the UI. * * TODO: register a broadcast receiver that calls updateUI() when it receives this intent */ public static final String UPDATE_INTENT = "com.android.settings.InjectedLocationSettingChanged"; /** * Returns a list with one {@link InjectedSetting} object for each {@link android.app.Service} * that responds to {@link #RECEIVER_INTENT} and provides the expected setting metadata. * * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. * * TODO: sort alphabetically * * TODO: unit test */ public static List<InjectedSetting> getSettings(Context context) { PackageManager pm = context.getPackageManager(); Intent receiverIntent = new Intent(RECEIVER_INTENT); List<ResolveInfo> resolveInfos = pm.queryIntentServices(receiverIntent, PackageManager.GET_META_DATA); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Found services: " + resolveInfos); } List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size()); for (ResolveInfo receiver : resolveInfos) { try { InjectedSetting info = parseServiceInfo(receiver, pm); if (info == null) { Log.w(TAG, "Unable to load service info " + receiver); } else { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Loaded service info: " + info); } settings.add(info); } } catch (XmlPullParserException e) { Log.w(TAG, "Unable to load service info " + receiver, e); } catch (IOException e) { Log.w(TAG, "Unable to load service info " + receiver, e); } } return settings; } /** * Parses {@link InjectedSetting} from the attributes of the {@link #META_DATA_NAME} tag. * * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. */ private static InjectedSetting parseServiceInfo(ResolveInfo service, PackageManager pm) throws XmlPullParserException, IOException { ServiceInfo si = service.serviceInfo; XmlResourceParser parser = null; try { parser = si.loadXmlMetaData(pm, META_DATA_NAME); if (parser == null) { throw new XmlPullParserException("No " + META_DATA_NAME + " meta-data for " + service + ": " + si); } AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { } String nodeName = parser.getName(); if (!ATTRIBUTES_NAME.equals(nodeName)) { throw new XmlPullParserException("Meta-data does not start with " + ATTRIBUTES_NAME + " tag"); } Resources res = pm.getResourcesForApplication(si.applicationInfo); return parseAttributes(si.packageName, si.name, res, attrs); } catch (PackageManager.NameNotFoundException e) { throw new XmlPullParserException( "Unable to load resources for package " + si.packageName); } finally { if (parser != null) { parser.close(); } } } private static InjectedSetting parseAttributes( String packageName, String className, Resources res, AttributeSet attrs) { TypedArray sa = res.obtainAttributes(attrs, R.styleable.InjectedLocationSetting); try { // Note that to help guard against malicious string injection, we do not allow dynamic // specification of the label (setting title) final int labelId = sa.getResourceId(R.styleable.InjectedLocationSetting_label, 0); final String label = sa.getString(R.styleable.InjectedLocationSetting_label); final int iconId = sa.getResourceId(R.styleable.InjectedLocationSetting_icon, 0); final String settingsActivity = sa.getString(R.styleable.InjectedLocationSetting_settingsActivity); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "parsed labelId: " + labelId + ", label: " + label + ", iconId: " + iconId); } if (labelId == 0 || TextUtils.isEmpty(label) || TextUtils.isEmpty(settingsActivity)) { return null; } return new InjectedSetting(packageName, className, label, iconId, settingsActivity); } finally { sa.recycle(); } } /** * Add settings that other apps have injected. * * TODO: extract InjectedLocationSettingGetter that returns an iterable over * InjectedSetting objects, so that this class can focus on UI */ public static void addInjectedSettings(PreferenceGroup group, Context context, PreferenceManager preferenceManager) { Iterable<InjectedSetting> settings = getSettings(context); for (InjectedSetting setting : settings) { Preference pref = addServiceSetting(context, group, setting, preferenceManager); // TODO: to prevent churn from multiple live broadcast receivers, don't trigger // the next update until the sooner of: the current update completes or 1-2 seconds // after the current update was started. updateSetting(context, pref, setting); } } /** * Adds an injected setting to the root with status "Loading...". */ private static PreferenceScreen addServiceSetting(Context context, PreferenceGroup group, InjectedSetting info, PreferenceManager preferenceManager) { PreferenceScreen screen = preferenceManager.createPreferenceScreen(context); screen.setTitle(info.title); screen.setSummary("Loading..."); PackageManager pm = context.getPackageManager(); Drawable icon = pm.getDrawable(info.packageName, info.iconId, null); screen.setIcon(icon); Intent settingIntent = new Intent(); settingIntent.setClassName(info.packageName, info.settingsActivity); screen.setIntent(settingIntent); group.addPreference(screen); return screen; } /** * Ask the receiver for the current status for the setting, and display it when it replies. */ private static void updateSetting(Context context, final Preference pref, final InjectedSetting info) { 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, info + ": received " + msg + ", bundle: " + bundle); } pref.setSummary(status); pref.setEnabled(enabled); } }; Messenger messenger = new Messenger(handler); Intent receiverIntent = info.getServiceIntent(); receiverIntent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, info + ": sending rcv-intent: " + receiverIntent + ", handler: " + handler); } context.startService(receiverIntent); } } Loading
res/values/strings.xml +3 −0 Original line number Diff line number Diff line Loading @@ -2380,6 +2380,9 @@ <string name="location_mode_battery_saving_description">Use Wi\u2011Fi and mobile networks to estimate location</string> <!-- [CHAR LIMIT=130] Location mode screen, description for sensors only mode --> <string name="location_mode_sensors_only_description">Use GPS to pinpoint your location</string> <!-- [CHAR LIMIT=130] Location mode screen, temporary value to show as the status of a location setting injected by an external app while the app is being queried for the actual value --> <string name="location_loading_injected_setting">Retrieving…</string> <!-- [CHAR LIMIT=30] Security & location settings screen, setting check box label for Google location service (cell ID, wifi, etc.) --> <string name="location_network_based">Wi\u2011Fi & mobile network location</string> Loading
src/com/android/settings/location/InjectedSetting.java 0 → 100644 +84 −0 Original line number Diff line number Diff line /* * Copyright (C) 2013 The Android Open Source 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 com.android.settings.location; import android.content.Intent; /** * Specifies a setting that is being injected into Settings > Location > Location services. * * @see android.location.SettingInjectorService */ class InjectedSetting { /** * Package for the subclass of {@link android.location.SettingInjectorService} and for the * settings activity. */ public final String packageName; /** * Class name for the subclass of {@link android.location.SettingInjectorService} that * specifies dynamic values for the location setting. */ public final String className; /** * The {@link android.preference.Preference#getTitle()} value. */ public final String title; /** * The {@link android.preference.Preference#getIcon()} value. */ public final int iconId; /** * The activity to launch to allow the user to modify the settings value. Assumed to be in the * {@link #packageName} package. */ public final String settingsActivity; public InjectedSetting(String packageName, String className, String title, int iconId, String settingsActivity) { this.packageName = packageName; this.className = className; this.title = title; this.iconId = iconId; this.settingsActivity = settingsActivity; } @Override public String toString() { return "InjectedSetting{" + "mPackageName='" + packageName + '\'' + ", mClassName='" + className + '\'' + ", label=" + title + ", iconId=" + iconId + ", settingsActivity='" + settingsActivity + '\'' + '}'; } /** * Returns the intent to start the {@link #className} service. */ public Intent getServiceIntent() { Intent intent = new Intent(); intent.setClassName(packageName, className); return intent; } }
src/com/android/settings/location/LocationSettings.java +2 −0 Original line number Diff line number Diff line Loading @@ -106,6 +106,8 @@ public class LocationSettings extends LocationSettingsBase RecentLocationApps recentApps = new RecentLocationApps(activity); recentApps.fillAppList(mRecentLocationRequests); SettingsInjector.addInjectedSettings(mLocationServices, activity, getPreferenceManager()); if (activity instanceof PreferenceActivity) { PreferenceActivity preferenceActivity = (PreferenceActivity) activity; // Only show the master switch when we're not in multi-pane mode, and not being used as Loading
src/com/android/settings/location/SettingsInjector.java 0 → 100644 +266 −0 Original line number Diff line number Diff line /* * Copyright (C) 2013 The Android Open Source 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 com.android.settings.location; import android.R; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.location.SettingInjectorService; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Messenger; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. * * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that * class directly because it is not a good match for our use case: we do not need the caching, and * so do not want the additional resource hit at app install/upgrade time; and we would have to * suppress the tie-breaking between multiple services reporting settings with the same name. * Code-sharing would require extracting {@link * android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources, * String, android.util.AttributeSet)} into an interface, which didn't seem worth it. */ class SettingsInjector { private static final String TAG = "SettingsInjector"; /** * Intent action marking the receiver as injecting a setting */ public static final String RECEIVER_INTENT = "com.android.settings.InjectedLocationSetting"; /** * Name of the meta-data tag used to specify the resource file that includes the settings * attributes. */ public static final String META_DATA_NAME = "com.android.settings.InjectedLocationSetting"; /** * Name of the XML tag that includes the attributes for the setting. */ public static final String ATTRIBUTES_NAME = "injected-location-setting"; /** * Intent action a client should broadcast when the value of one of its injected settings has * changed, so that the setting can be updated in the UI. * * TODO: register a broadcast receiver that calls updateUI() when it receives this intent */ public static final String UPDATE_INTENT = "com.android.settings.InjectedLocationSettingChanged"; /** * Returns a list with one {@link InjectedSetting} object for each {@link android.app.Service} * that responds to {@link #RECEIVER_INTENT} and provides the expected setting metadata. * * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. * * TODO: sort alphabetically * * TODO: unit test */ public static List<InjectedSetting> getSettings(Context context) { PackageManager pm = context.getPackageManager(); Intent receiverIntent = new Intent(RECEIVER_INTENT); List<ResolveInfo> resolveInfos = pm.queryIntentServices(receiverIntent, PackageManager.GET_META_DATA); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Found services: " + resolveInfos); } List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size()); for (ResolveInfo receiver : resolveInfos) { try { InjectedSetting info = parseServiceInfo(receiver, pm); if (info == null) { Log.w(TAG, "Unable to load service info " + receiver); } else { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Loaded service info: " + info); } settings.add(info); } } catch (XmlPullParserException e) { Log.w(TAG, "Unable to load service info " + receiver, e); } catch (IOException e) { Log.w(TAG, "Unable to load service info " + receiver, e); } } return settings; } /** * Parses {@link InjectedSetting} from the attributes of the {@link #META_DATA_NAME} tag. * * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. */ private static InjectedSetting parseServiceInfo(ResolveInfo service, PackageManager pm) throws XmlPullParserException, IOException { ServiceInfo si = service.serviceInfo; XmlResourceParser parser = null; try { parser = si.loadXmlMetaData(pm, META_DATA_NAME); if (parser == null) { throw new XmlPullParserException("No " + META_DATA_NAME + " meta-data for " + service + ": " + si); } AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { } String nodeName = parser.getName(); if (!ATTRIBUTES_NAME.equals(nodeName)) { throw new XmlPullParserException("Meta-data does not start with " + ATTRIBUTES_NAME + " tag"); } Resources res = pm.getResourcesForApplication(si.applicationInfo); return parseAttributes(si.packageName, si.name, res, attrs); } catch (PackageManager.NameNotFoundException e) { throw new XmlPullParserException( "Unable to load resources for package " + si.packageName); } finally { if (parser != null) { parser.close(); } } } private static InjectedSetting parseAttributes( String packageName, String className, Resources res, AttributeSet attrs) { TypedArray sa = res.obtainAttributes(attrs, R.styleable.InjectedLocationSetting); try { // Note that to help guard against malicious string injection, we do not allow dynamic // specification of the label (setting title) final int labelId = sa.getResourceId(R.styleable.InjectedLocationSetting_label, 0); final String label = sa.getString(R.styleable.InjectedLocationSetting_label); final int iconId = sa.getResourceId(R.styleable.InjectedLocationSetting_icon, 0); final String settingsActivity = sa.getString(R.styleable.InjectedLocationSetting_settingsActivity); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "parsed labelId: " + labelId + ", label: " + label + ", iconId: " + iconId); } if (labelId == 0 || TextUtils.isEmpty(label) || TextUtils.isEmpty(settingsActivity)) { return null; } return new InjectedSetting(packageName, className, label, iconId, settingsActivity); } finally { sa.recycle(); } } /** * Add settings that other apps have injected. * * TODO: extract InjectedLocationSettingGetter that returns an iterable over * InjectedSetting objects, so that this class can focus on UI */ public static void addInjectedSettings(PreferenceGroup group, Context context, PreferenceManager preferenceManager) { Iterable<InjectedSetting> settings = getSettings(context); for (InjectedSetting setting : settings) { Preference pref = addServiceSetting(context, group, setting, preferenceManager); // TODO: to prevent churn from multiple live broadcast receivers, don't trigger // the next update until the sooner of: the current update completes or 1-2 seconds // after the current update was started. updateSetting(context, pref, setting); } } /** * Adds an injected setting to the root with status "Loading...". */ private static PreferenceScreen addServiceSetting(Context context, PreferenceGroup group, InjectedSetting info, PreferenceManager preferenceManager) { PreferenceScreen screen = preferenceManager.createPreferenceScreen(context); screen.setTitle(info.title); screen.setSummary("Loading..."); PackageManager pm = context.getPackageManager(); Drawable icon = pm.getDrawable(info.packageName, info.iconId, null); screen.setIcon(icon); Intent settingIntent = new Intent(); settingIntent.setClassName(info.packageName, info.settingsActivity); screen.setIntent(settingIntent); group.addPreference(screen); return screen; } /** * Ask the receiver for the current status for the setting, and display it when it replies. */ private static void updateSetting(Context context, final Preference pref, final InjectedSetting info) { 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, info + ": received " + msg + ", bundle: " + bundle); } pref.setSummary(status); pref.setEnabled(enabled); } }; Messenger messenger = new Messenger(handler); Intent receiverIntent = info.getServiceIntent(); receiverIntent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, info + ": sending rcv-intent: " + receiverIntent + ", handler: " + handler); } context.startService(receiverIntent); } }