SettingInjectorService.java revision 546113d4c290f36bf21b1e9c7b93d1592df17fce
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.util.Log;
26
27/**
28 * Dynamically specifies the summary (subtitle) and enabled status of a preference injected into
29 * the list of location services displayed by the system settings app.
30 *
31 * The location services list is intended for use only by preferences that affect multiple apps from
32 * the same developer. Location settings that apply only to one app should be shown within that app,
33 * rather than in the system settings.
34 *
35 * To add a preference to the list, a subclass of {@link SettingInjectorService} must be declared in
36 * the manifest as so:
37 *
38 * <pre>
39 *     &lt;service android:name="com.example.android.injector.MyInjectorService" &gt;
40 *         &lt;intent-filter&gt;
41 *             &lt;action android:name="android.location.SettingInjectorService" /&gt;
42 *         &lt;/intent-filter&gt;
43 *
44 *         &lt;meta-data
45 *             android:name="android.location.SettingInjectorService"
46 *             android:resource="@xml/my_injected_location_setting" /&gt;
47 *     &lt;/service&gt;
48 * </pre>
49 * The resource file specifies the static data for the setting:
50 * <pre>
51 *     &lt;injected-location-setting xmlns:android="http://schemas.android.com/apk/res/android"
52 *         android:title="@string/injected_setting_title"
53 *         android:icon="@drawable/ic_acme_corp"
54 *         android:settingsActivity="com.example.android.injector.MySettingActivity"
55 *     /&gt;
56 * </pre>
57 * Here:
58 * <ul>
59 *     <li>title: The {@link android.preference.Preference#getTitle()} value. The title should make
60 *     it clear which apps are affected by the setting, typically by including the name of the
61 *     developer. For example, "Acme Corp. ads preferences." </li>
62 *
63 *     <li>icon: The {@link android.preference.Preference#getIcon()} value. Typically this will be a
64 *     generic icon for the developer rather than the icon for an individual app.</li>
65 *
66 *     <li>settingsActivity: the activity which is launched to allow the user to modify the setting
67 *     value.  The activity must be in the same package as the subclass of
68 *     {@link SettingInjectorService}. The activity should use your own branding to help emphasize
69 *     to the user that it is not part of the system settings.</li>
70 * </ul>
71 *
72 * To ensure a good user experience, the average time from the start of
73 * {@link #startService(Intent)} to the end of {@link #onHandleIntent(Intent)} should be less
74 * than 300 msec even if your app is not already in memory. This means that both your
75 * {@link android.app.Application#onCreate()} and your {@link #getStatus()} must be fast. If
76 * either is slow, it can delay the display of settings values for other apps as well.
77 *
78 * For compactness, only one copy of a given setting should be injected. If each account has a
79 * distinct value for the setting, then the {@link #getStatus()} value should represent a summary of
80 * the state across all of the accounts and {@code settingsActivity} should display the value for
81 * each account.
82 */
83// TODO: is there a public list of supported locales?
84// TODO: is there a public list of guidelines for settings text?
85// TODO: would a bound service be better? E.g., we could just disconnect if a service took too long
86public abstract class SettingInjectorService extends IntentService {
87
88    private static final String TAG = "SettingInjectorService";
89
90    /**
91     * Intent action that must be declared in the manifest for the subclass. Used to start the
92     * service to read the dynamic status for the setting.
93     */
94    public static final String ACTION_SERVICE_INTENT = "android.location.SettingInjectorService";
95
96    /**
97     * Name of the meta-data tag used to specify the resource file that includes the settings
98     * attributes.
99     */
100    public static final String META_DATA_NAME = "android.location.SettingInjectorService";
101
102    /**
103     * Name of the XML tag that includes the attributes for the setting.
104     */
105    public static final String ATTRIBUTES_NAME = "injected-location-setting";
106
107    /**
108     * Intent action a client should broadcast when the value of one of its injected settings has
109     * changed, so that the setting can be updated in the UI.
110     */
111    public static final String ACTION_INJECTED_SETTING_CHANGED =
112            "android.location.InjectedSettingChanged";
113
114    /**
115     * Name of the bundle key for the string specifying the summary for the setting (e.g., "ON" or
116     * "OFF").
117     *
118     * @hide
119     */
120    public static final String SUMMARY_KEY = "summary";
121
122    /**
123     * Name of the bundle key for the string specifying whether the setting is currently enabled.
124     *
125     * @hide
126     */
127    public static final String ENABLED_KEY = "enabled";
128
129    /**
130     * Name of the intent key used to specify the messenger
131     *
132     * @hide
133     */
134    public static final String MESSENGER_KEY = "messenger";
135
136    private final String mName;
137
138    /**
139     * Constructor.
140     *
141     * @param name used to name the worker thread and in log messages
142     */
143    public SettingInjectorService(String name) {
144        super(name);
145        mName = name;
146    }
147
148    @Override
149    final protected void onHandleIntent(Intent intent) {
150        // Get messenger first to ensure intent doesn't get messed with (in case we later decide
151        // to pass intent into getStatus())
152        Messenger messenger = intent.getParcelableExtra(MESSENGER_KEY);
153
154        Status status;
155        try {
156            status = getStatus();
157        } catch (RuntimeException e) {
158            Log.e(TAG, mName + ": error getting status", e);
159            status = null;
160        }
161
162        // Send the status back to the caller via the messenger
163        Message message = Message.obtain();
164        Bundle bundle = new Bundle();
165        if (status != null) {
166            bundle.putString(SUMMARY_KEY, status.summary);
167            bundle.putBoolean(ENABLED_KEY, status.enabled);
168        }
169        message.setData(bundle);
170
171        if (Log.isLoggable(TAG, Log.DEBUG)) {
172            Log.d(TAG, mName + ": received " + intent + " and " + status
173                    + ", sending message: " + message);
174        }
175        try {
176            messenger.send(message);
177        } catch (RemoteException e) {
178            Log.e(TAG, mName + ": sending status failed", e);
179        }
180    }
181
182    /**
183     * Reads the status of the setting. Should not perform unpredictably-long operations such as
184     * network access--see the running-time comments in the class-level javadoc.
185     *
186     * @return the status of the setting value
187     */
188    protected abstract Status getStatus();
189
190    /**
191     * Dynamic characteristics of an injected location setting.
192     */
193    public static final class Status {
194
195        /**
196         * The {@link android.preference.Preference#getSummary()} value.
197         *
198         * @hide
199         */
200        public final String summary;
201
202        /**
203         * The {@link android.preference.Preference#isEnabled()} value.
204         *
205         * @hide
206         */
207        public final boolean enabled;
208
209        /**
210         * Constructor.
211         * <p/>
212         * Note that to prevent churn in the settings list, there is no support for dynamically
213         * choosing to hide a setting. Instead you should provide a {@code enabled} value of false,
214         * which will disable the setting and its link to your setting activity. One reason why you
215         * might choose to do this is if {@link android.provider.Settings.Secure#LOCATION_MODE}
216         * is {@link android.provider.Settings.Secure#LOCATION_MODE_OFF}.
217         *
218         * It is possible that the user may click on the setting before you return a false value for
219         * {@code enabled}, so your settings activity must handle the case where it is invoked even
220         * though the setting is disabled. The simplest approach may be to simply call
221         * {@link android.app.Activity#finish()} when disabled.
222         *
223         * @param summary the {@link android.preference.Preference#getSummary()} value (allowed to
224         *                be null or empty)
225         * @param enabled the {@link android.preference.Preference#isEnabled()} value
226         */
227        public Status(String summary, boolean enabled) {
228            this.summary = summary;
229            this.enabled = enabled;
230        }
231
232        @Override
233        public String toString() {
234            return "Status{summary='" + summary + '\'' + ", enabled=" + enabled + '}';
235        }
236    }
237}
238