Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e2921c95 authored by Tom O'Neill's avatar Tom O'Neill
Browse files

Settings app supports location settings injection

- Partial fix for b/10287745

Change-Id: Ia5eb05670957125e70717c86a686a54b77b22455
parent 2f219c51
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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 &amp; mobile network location</string>
+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;
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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
+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);
    }
}