am 0574c26c: am cabe7e82: Merge "Settings app supports location settings injection" into klp-dev

* commit '0574c26c36b1b760d80107fb4171cdcb642aabf1':
  Settings app supports location settings injection
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8147396..6f242ca 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2391,6 +2391,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 &amp; mobile network location</string>
diff --git a/src/com/android/settings/location/InjectedSetting.java b/src/com/android/settings/location/InjectedSetting.java
new file mode 100644
index 0000000..01d3236
--- /dev/null
+++ b/src/com/android/settings/location/InjectedSetting.java
@@ -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;
+    }
+}
diff --git a/src/com/android/settings/location/LocationSettings.java b/src/com/android/settings/location/LocationSettings.java
index 97907de..2cbb798 100644
--- a/src/com/android/settings/location/LocationSettings.java
+++ b/src/com/android/settings/location/LocationSettings.java
@@ -106,6 +106,8 @@
         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
diff --git a/src/com/android/settings/location/SettingsInjector.java b/src/com/android/settings/location/SettingsInjector.java
new file mode 100644
index 0000000..258f3fd
--- /dev/null
+++ b/src/com/android/settings/location/SettingsInjector.java
@@ -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);
+    }
+}