1 /* 2 * Copyright (C) 2015 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 package android.service.quicksettings; 17 18 import android.Manifest; 19 import android.annotation.SdkConstant; 20 import android.annotation.SdkConstant.SdkConstantType; 21 import android.annotation.SystemApi; 22 import android.app.Dialog; 23 import android.app.Service; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.graphics.drawable.Icon; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.os.RemoteException; 33 import android.view.View; 34 import android.view.View.OnAttachStateChangeListener; 35 import android.view.WindowManager; 36 37 /** 38 * A TileService provides the user a tile that can be added to Quick Settings. 39 * Quick Settings is a space provided that allows the user to change settings and 40 * take quick actions without leaving the context of their current app. 41 * 42 * <p>The lifecycle of a TileService is different from some other services in 43 * that it may be unbound during parts of its lifecycle. Any of the following 44 * lifecycle events can happen indepently in a separate binding/creation of the 45 * service.</p> 46 * 47 * <ul> 48 * <li>When a tile is added by the user its TileService will be bound to and 49 * {@link #onTileAdded()} will be called.</li> 50 * 51 * <li>When a tile should be up to date and listing will be indicated by 52 * {@link #onStartListening()} and {@link #onStopListening()}.</li> 53 * 54 * <li>When the user removes a tile from Quick Settings {@link #onTileRemoved()} 55 * will be called.</li> 56 * </ul> 57 * <p>TileService will be detected by tiles that match the {@value #ACTION_QS_TILE} 58 * and require the permission "android.permission.BIND_QUICK_SETTINGS_TILE". 59 * The label and icon for the service will be used as the default label and 60 * icon for the tile. Here is an example TileService declaration.</p> 61 * <pre class="prettyprint"> 62 * {@literal 63 * <service 64 * android:name=".MyQSTileService" 65 * android:label="@string/my_default_tile_label" 66 * android:icon="@drawable/my_default_icon_label" 67 * android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> 68 * <intent-filter> 69 * <action android:name="android.service.quicksettings.action.QS_TILE" /> 70 * </intent-filter> 71 * </service>} 72 * </pre> 73 * 74 * @see Tile Tile for details about the UI of a Quick Settings Tile. 75 */ 76 public class TileService extends Service { 77 78 /** 79 * An activity that provides a user interface for adjusting TileService preferences. 80 * Optional but recommended for apps that implement a TileService. 81 */ 82 @SdkConstant(SdkConstantType.INTENT_CATEGORY) 83 public static final String ACTION_QS_TILE_PREFERENCES 84 = "android.service.quicksettings.action.QS_TILE_PREFERENCES"; 85 86 /** 87 * Action that identifies a Service as being a TileService. 88 */ 89 public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE"; 90 91 /** 92 * Meta-data for tile definition to set a tile into active mode. 93 * <p> 94 * Active mode is for tiles which already listen and keep track of their state in their 95 * own process. These tiles may request to send an update to the System while their process 96 * is alive using {@link #requestListeningState}. The System will only bind these tiles 97 * on its own when a click needs to occur. 98 * 99 * To make a TileService an active tile, set this meta-data to true on the TileService's 100 * manifest declaration. 101 * <pre class="prettyprint"> 102 * {@literal 103 * <meta-data android:name="android.service.quicksettings.ACTIVE_TILE" 104 * android:value="true" /> 105 * } 106 * </pre> 107 */ 108 public static final String META_DATA_ACTIVE_TILE 109 = "android.service.quicksettings.ACTIVE_TILE"; 110 111 /** 112 * Used to notify SysUI that Listening has be requested. 113 * @hide 114 */ 115 public static final String ACTION_REQUEST_LISTENING 116 = "android.service.quicksettings.action.REQUEST_LISTENING"; 117 118 /** 119 * @hide 120 */ 121 public static final String EXTRA_SERVICE = "service"; 122 123 /** 124 * @hide 125 */ 126 public static final String EXTRA_TOKEN = "token"; 127 128 /** 129 * @hide 130 */ 131 public static final String EXTRA_COMPONENT = "android.service.quicksettings.extra.COMPONENT"; 132 133 private final H mHandler = new H(Looper.getMainLooper()); 134 135 private boolean mListening = false; 136 private Tile mTile; 137 private IBinder mToken; 138 private IQSService mService; 139 private Runnable mUnlockRunnable; 140 private IBinder mTileToken; 141 142 @Override 143 public void onDestroy() { 144 if (mListening) { 145 onStopListening(); 146 mListening = false; 147 } 148 super.onDestroy(); 149 } 150 151 /** 152 * Called when the user adds this tile to Quick Settings. 153 * <p/> 154 * Note that this is not guaranteed to be called between {@link #onCreate()} 155 * and {@link #onStartListening()}, it will only be called when the tile is added 156 * and not on subsequent binds. 157 */ 158 public void onTileAdded() { 159 } 160 161 /** 162 * Called when the user removes this tile from Quick Settings. 163 */ 164 public void onTileRemoved() { 165 } 166 167 /** 168 * Called when this tile moves into a listening state. 169 * <p/> 170 * When this tile is in a listening state it is expected to keep the 171 * UI up to date. Any listeners or callbacks needed to keep this tile 172 * up to date should be registered here and unregistered in {@link #onStopListening()}. 173 * 174 * @see #getQsTile() 175 * @see Tile#updateTile() 176 */ 177 public void onStartListening() { 178 } 179 180 /** 181 * Called when this tile moves out of the listening state. 182 */ 183 public void onStopListening() { 184 } 185 186 /** 187 * Called when the user clicks on this tile. 188 */ 189 public void onClick() { 190 } 191 192 /** 193 * Sets an icon to be shown in the status bar. 194 * <p> 195 * The icon will be displayed before all other icons. Can only be called between 196 * {@link #onStartListening} and {@link #onStopListening}. Can only be called by system apps. 197 * 198 * @param icon The icon to be displayed, null to hide 199 * @param contentDescription Content description of the icon to be displayed 200 * @hide 201 */ 202 @SystemApi 203 public final void setStatusIcon(Icon icon, String contentDescription) { 204 if (mService != null) { 205 try { 206 mService.updateStatusIcon(mTileToken, icon, contentDescription); 207 } catch (RemoteException e) { 208 } 209 } 210 } 211 212 /** 213 * Used to show a dialog. 214 * 215 * This will collapse the Quick Settings panel and show the dialog. 216 * 217 * @param dialog Dialog to show. 218 * 219 * @see #isLocked() 220 */ 221 public final void showDialog(Dialog dialog) { 222 dialog.getWindow().getAttributes().token = mToken; 223 dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_QS_DIALOG); 224 dialog.getWindow().getDecorView().addOnAttachStateChangeListener( 225 new OnAttachStateChangeListener() { 226 @Override 227 public void onViewAttachedToWindow(View v) { 228 } 229 230 @Override 231 public void onViewDetachedFromWindow(View v) { 232 try { 233 mService.onDialogHidden(mTileToken); 234 } catch (RemoteException e) { 235 } 236 } 237 }); 238 dialog.show(); 239 try { 240 mService.onShowDialog(mTileToken); 241 } catch (RemoteException e) { 242 } 243 } 244 245 /** 246 * Prompts the user to unlock the device before executing the Runnable. 247 * <p> 248 * The user will be prompted for their current security method if applicable 249 * and if successful, runnable will be executed. The Runnable will not be 250 * executed if the user fails to unlock the device or cancels the operation. 251 */ 252 public final void unlockAndRun(Runnable runnable) { 253 mUnlockRunnable = runnable; 254 try { 255 mService.startUnlockAndRun(mTileToken); 256 } catch (RemoteException e) { 257 } 258 } 259 260 /** 261 * Checks if the device is in a secure state. 262 * 263 * TileServices should detect when the device is secure and change their behavior 264 * accordingly. 265 * 266 * @return true if the device is secure. 267 */ 268 public final boolean isSecure() { 269 try { 270 return mService.isSecure(); 271 } catch (RemoteException e) { 272 return true; 273 } 274 } 275 276 /** 277 * Checks if the lock screen is showing. 278 * 279 * When a device is locked, then {@link #showDialog} will not present a dialog, as it will 280 * be under the lock screen. If the behavior of the Tile is safe to do while locked, 281 * then the user should use {@link #startActivity} to launch an activity on top of the lock 282 * screen, otherwise the tile should use {@link #unlockAndRun(Runnable)} to give the 283 * user their security challenge. 284 * 285 * @return true if the device is locked. 286 */ 287 public final boolean isLocked() { 288 try { 289 return mService.isLocked(); 290 } catch (RemoteException e) { 291 return true; 292 } 293 } 294 295 /** 296 * Start an activity while collapsing the panel. 297 */ 298 public final void startActivityAndCollapse(Intent intent) { 299 startActivity(intent); 300 try { 301 mService.onStartActivity(mTileToken); 302 } catch (RemoteException e) { 303 } 304 } 305 306 /** 307 * Gets the {@link Tile} for this service. 308 * <p/> 309 * This tile may be used to get or set the current state for this 310 * tile. This tile is only valid for updates between {@link #onStartListening()} 311 * and {@link #onStopListening()}. 312 */ 313 public final Tile getQsTile() { 314 return mTile; 315 } 316 317 @Override 318 public IBinder onBind(Intent intent) { 319 mService = IQSService.Stub.asInterface(intent.getIBinderExtra(EXTRA_SERVICE)); 320 mTileToken = intent.getIBinderExtra(EXTRA_TOKEN); 321 try { 322 mTile = mService.getTile(mTileToken); 323 } catch (RemoteException e) { 324 throw new RuntimeException("Unable to reach IQSService", e); 325 } 326 if (mTile != null) { 327 mTile.setService(mService, mTileToken); 328 mHandler.sendEmptyMessage(H.MSG_START_SUCCESS); 329 } 330 return new IQSTileService.Stub() { 331 @Override 332 public void onTileRemoved() throws RemoteException { 333 mHandler.sendEmptyMessage(H.MSG_TILE_REMOVED); 334 } 335 336 @Override 337 public void onTileAdded() throws RemoteException { 338 mHandler.sendEmptyMessage(H.MSG_TILE_ADDED); 339 } 340 341 @Override 342 public void onStopListening() throws RemoteException { 343 mHandler.sendEmptyMessage(H.MSG_STOP_LISTENING); 344 } 345 346 @Override 347 public void onStartListening() throws RemoteException { 348 mHandler.sendEmptyMessage(H.MSG_START_LISTENING); 349 } 350 351 @Override 352 public void onClick(IBinder wtoken) throws RemoteException { 353 mHandler.obtainMessage(H.MSG_TILE_CLICKED, wtoken).sendToTarget(); 354 } 355 356 @Override 357 public void onUnlockComplete() throws RemoteException{ 358 mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE); 359 } 360 }; 361 } 362 363 private class H extends Handler { 364 private static final int MSG_START_LISTENING = 1; 365 private static final int MSG_STOP_LISTENING = 2; 366 private static final int MSG_TILE_ADDED = 3; 367 private static final int MSG_TILE_REMOVED = 4; 368 private static final int MSG_TILE_CLICKED = 5; 369 private static final int MSG_UNLOCK_COMPLETE = 6; 370 private static final int MSG_START_SUCCESS = 7; 371 372 public H(Looper looper) { 373 super(looper); 374 } 375 376 @Override 377 public void handleMessage(Message msg) { 378 switch (msg.what) { 379 case MSG_TILE_ADDED: 380 TileService.this.onTileAdded(); 381 break; 382 case MSG_TILE_REMOVED: 383 if (mListening) { 384 mListening = false; 385 TileService.this.onStopListening(); 386 } 387 TileService.this.onTileRemoved(); 388 break; 389 case MSG_STOP_LISTENING: 390 if (mListening) { 391 mListening = false; 392 TileService.this.onStopListening(); 393 } 394 break; 395 case MSG_START_LISTENING: 396 if (!mListening) { 397 mListening = true; 398 TileService.this.onStartListening(); 399 } 400 break; 401 case MSG_TILE_CLICKED: 402 mToken = (IBinder) msg.obj; 403 TileService.this.onClick(); 404 break; 405 case MSG_UNLOCK_COMPLETE: 406 if (mUnlockRunnable != null) { 407 mUnlockRunnable.run(); 408 } 409 break; 410 case MSG_START_SUCCESS: 411 try { 412 mService.onStartSuccessful(mTileToken); 413 } catch (RemoteException e) { 414 } 415 break; 416 } 417 } 418 } 419 420 /** 421 * Requests that a tile be put in the listening state so it can send an update. 422 * 423 * This method is only applicable to tiles that have {@link #META_DATA_ACTIVE_TILE} defined 424 * as true on their TileService Manifest declaration, and will do nothing otherwise. 425 */ 426 public static final void requestListeningState(Context context, ComponentName component) { 427 Intent intent = new Intent(ACTION_REQUEST_LISTENING); 428 intent.putExtra(EXTRA_COMPONENT, component); 429 context.sendBroadcast(intent, Manifest.permission.BIND_QUICK_SETTINGS_TILE); 430 } 431 } 432