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

Commit cabe7e82 authored by Tom O'Neill's avatar Tom O'Neill Committed by Android (Google) Code Review
Browse files

Merge "Settings app supports location settings injection" into klp-dev

parents cd3eb97c e2921c95
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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>
+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);
    }
}