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 * <service android:name="com.example.android.injector.MyInjectorService" > 43 * <intent-filter> 44 * <action android:name="android.location.SettingInjectorService" /> 45 * </intent-filter> 46 * 47 * <meta-data 48 * android:name="android.location.SettingInjectorService" 49 * android:resource="@xml/my_injected_location_setting" /> 50 * </service> 51 * </pre> 52 * The resource file specifies the static data for the setting: 53 * <pre> 54 * <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 * /> 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