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