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