Settings app supports location settings injection
- Partial fix for b/10287745 Change-Id: Ia5eb05670957125e70717c86a686a54b77b22455
This commit is contained in:
@@ -2380,6 +2380,9 @@
|
|||||||
<string name="location_mode_battery_saving_description">Use Wi\u2011Fi and mobile networks to estimate location</string>
|
<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 -->
|
<!-- [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>
|
<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.) -->
|
<!-- [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>
|
<string name="location_network_based">Wi\u2011Fi & mobile network location</string>
|
||||||
|
84
src/com/android/settings/location/InjectedSetting.java
Normal file
84
src/com/android/settings/location/InjectedSetting.java
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -106,6 +106,8 @@ public class LocationSettings extends LocationSettingsBase
|
|||||||
RecentLocationApps recentApps = new RecentLocationApps(activity);
|
RecentLocationApps recentApps = new RecentLocationApps(activity);
|
||||||
recentApps.fillAppList(mRecentLocationRequests);
|
recentApps.fillAppList(mRecentLocationRequests);
|
||||||
|
|
||||||
|
SettingsInjector.addInjectedSettings(mLocationServices, activity, getPreferenceManager());
|
||||||
|
|
||||||
if (activity instanceof PreferenceActivity) {
|
if (activity instanceof PreferenceActivity) {
|
||||||
PreferenceActivity preferenceActivity = (PreferenceActivity) activity;
|
PreferenceActivity preferenceActivity = (PreferenceActivity) activity;
|
||||||
// Only show the master switch when we're not in multi-pane mode, and not being used as
|
// Only show the master switch when we're not in multi-pane mode, and not being used as
|
||||||
|
266
src/com/android/settings/location/SettingsInjector.java
Normal file
266
src/com/android/settings/location/SettingsInjector.java
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user