SettingInjectorService.java revision 7f6f45723adea684529dd9b7465d798f10c3acbf
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.location;
18
19import android.app.IntentService;
20import android.content.Intent;
21import android.os.Bundle;
22import android.os.Message;
23import android.os.Messenger;
24import android.os.RemoteException;
25import android.preference.Preference;
26import android.util.Log;
27
28/**
29 * Dynamically specifies the summary (subtile) and enabled status of a preference injected into
30 * the "Settings > Location > Location services" list.
31 *
32 * The location services list is intended for use only by preferences that affect multiple apps from
33 * the same developer. Location settings that apply only to one app should be shown within that app,
34 * rather than in the system settings.
35 *
36 * To add a preference to the list, a subclass of {@link SettingInjectorService} must be declared in
37 * the manifest as so:
38 *
39 * <pre>
40 *     &lt;service android:name="com.example.android.injector.MyInjectorService" &gt;
41 *         &lt;intent-filter&gt;
42 *             &lt;action android:name="com.android.settings.InjectedLocationSetting" /&gt;
43 *         &lt;/intent-filter&gt;
44 *
45 *         &lt;meta-data
46 *             android:name="com.android.settings.InjectedLocationSetting"
47 *             android:resource="@xml/my_injected_location_setting" /&gt;
48 *     &lt;/service&gt;
49 * </pre>
50 * The resource file specifies the static data for the setting:
51 * <pre>
52 *     &lt;injected-location-setting xmlns:android="http://schemas.android.com/apk/res/android"
53 *         android:label="@string/injected_setting_label"
54 *         android:icon="@drawable/ic_launcher"
55 *         android:settingsActivity="com.example.android.injector.MySettingActivity"
56 *     /&gt;
57 * </pre>
58 * Here:
59 * <ul>
60 *     <li>label: The {@link Preference#getTitle()} value. The title should make it clear which apps
61 *     are affected by the setting, typically by including the name of the developer. For example,
62 *     "Acme Corp. ads preferences." </li>
63 *
64 *     <li>icon: The {@link Preference#getIcon()} value. Typically this will be a generic icon for
65 *     the developer rather than the icon for an individual app.</li>
66 *
67 *     <li>settingsActivity: the activity which is launched to allow the user to modify the setting
68 *     value  The activity must be in the same package as the subclass of
69 *     {@link SettingInjectorService}. The activity should use your own branding to help emphasize
70 *     to the user that it is not part of the system settings.</li>
71 * </ul>
72 *
73 * To ensure a good user experience, your {@link #onHandleIntent(Intent)} must complete within
74 * 200 msec even if your app is not already running. This means that both
75 * {@link android.app.Application#onCreate()} and {@link #getStatus()} must be fast. If you exceed
76 * this time, then this can delay the retrieval of settings status for other apps as well.
77 *
78 * For consistency, the label and {@link #getStatus()} values should be provided in all of the
79 * locales supported by the system settings app. The text should not contain offensive language.
80 *
81 * For compactness, only one copy of a given setting should be injected. If each account has a
82 * distinct value for the setting, then the {@link #getStatus()} value should represent a summary of
83 * the state across all of the accounts and {@code settingsActivity} should display the value for
84 * each account.
85 *
86 * Apps that violate these guidelines will be taken down from the Google Play Store and/or flagged
87 * as malware.
88 */
89// TODO: is there a public list of supported locales?
90// TODO: is there a public list of guidelines for settings text?
91// TODO: would a bound service be better? E.g., we could just disconnect if a service took too long
92public abstract class SettingInjectorService extends IntentService {
93
94    private static final String TAG = "SettingInjectorService";
95
96    /**
97     * Name of the bundle key for the string specifying the summary for the setting (e.g., "ON" or
98     * "OFF").
99     *
100     * @hide
101     */
102    public static final String SUMMARY_KEY = "summary";
103
104    /**
105     * Name of the bundle key for the string specifying whether the setting is currently enabled.
106     *
107     * @hide
108     */
109    public static final String ENABLED_KEY = "enabled";
110
111    /**
112     * Name of the intent key used to specify the messenger
113     *
114     * @hide
115     */
116    public static final String MESSENGER_KEY = "messenger";
117
118    /**
119     * Intent action a client should broadcast when the value of one of its injected settings has
120     * changed, so that the setting can be updated in the UI.
121     */
122    public static final String ACTION_INJECTED_SETTING_CHANGED =
123            "com.android.location.InjectedSettingChanged";
124
125    private final String mName;
126
127    /**
128     * Constructor.
129     *
130     * @param name used to name the worker thread and in log messages
131     */
132    public SettingInjectorService(String name) {
133        super(name);
134        mName = name;
135    }
136
137    @Override
138    final protected void onHandleIntent(Intent intent) {
139        // Get messenger first to ensure intent doesn't get messed with (in case we later decide
140        // to pass intent into getStatus())
141        Messenger messenger = intent.getParcelableExtra(MESSENGER_KEY);
142
143        Status status;
144        try {
145            status = getStatus();
146        } catch (RuntimeException e) {
147            Log.e(TAG, mName + ": error getting status", e);
148            status = null;
149        }
150
151        // Send the status back to the caller via the messenger
152        Message message = Message.obtain();
153        Bundle bundle = new Bundle();
154        if (status != null) {
155            bundle.putString(SUMMARY_KEY, status.summary);
156            bundle.putBoolean(ENABLED_KEY, status.enabled);
157        }
158        message.setData(bundle);
159
160        if (Log.isLoggable(TAG, Log.DEBUG)) {
161            Log.d(TAG, mName + ": received " + intent + " and " + status
162                    + ", sending message: " + message);
163        }
164        try {
165            messenger.send(message);
166        } catch (RemoteException e) {
167            Log.e(TAG, mName + ": sending status failed", e);
168        }
169    }
170
171    /**
172     * Reads the status of the setting.
173     */
174    protected abstract Status getStatus();
175
176    /**
177     * Dynamic characteristics of an injected location setting.
178     */
179    public static final class Status {
180
181        /**
182         * The {@link Preference#getSummary()} value
183         */
184        public final String summary;
185
186        /**
187         * The {@link Preference#isEnabled()} value
188         */
189        public final boolean enabled;
190
191        /**
192         * Constructor.
193         * <p/>
194         * Note that to prevent churn in the settings list, there is no support for dynamically
195         * choosing to hide a setting. Instead you should provide a {@code enabled} value of false,
196         * which will gray the setting out and disable the link from "Settings > Location"
197         * to your setting activity. One reason why you might choose to do this is if
198         * {@link android.provider.Settings.Secure#LOCATION_MODE}
199         * is {@link android.provider.Settings.Secure#LOCATION_MODE_OFF}.
200         *
201         * It is possible that the user may click on the setting before you return a false value for
202         * {@code enabled}, so your settings activity must handle the case where it is invoked even
203         * though the setting is disabled. The simplest approach may be to simply call
204         * {@link android.app.Activity#finish()} when disabled.
205         *
206         * @param summary the {@link Preference#getSummary()} value (allowed to be null or empty)
207         * @param enabled the {@link Preference#isEnabled()} value
208         */
209        public Status(String summary, boolean enabled) {
210            this.summary = summary;
211            this.enabled = enabled;
212        }
213
214        @Override
215        public String toString() {
216            return "Status{summary='" + summary + '\'' + ", enabled=" + enabled + '}';
217        }
218    }
219}
220