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.annotation.SdkConstant; 19 import android.annotation.SdkConstant.SdkConstantType; 20 import android.annotation.SystemApi; 21 import android.annotation.TestApi; 22 import android.app.Dialog; 23 import android.app.PendingIntent; 24 import android.app.Service; 25 import android.app.StatusBarManager; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.res.Resources; 30 import android.graphics.drawable.Icon; 31 import android.os.Build; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Message; 36 import android.os.RemoteException; 37 import android.util.Log; 38 import android.view.View; 39 import android.view.View.OnAttachStateChangeListener; 40 import android.view.WindowManager; 41 42 import com.android.internal.R; 43 44 /** 45 * A TileService provides the user a tile that can be added to Quick Settings. 46 * Quick Settings is a space provided that allows the user to change settings and 47 * take quick actions without leaving the context of their current app. 48 * 49 * <p>The lifecycle of a TileService is different from some other services in 50 * that it may be unbound during parts of its lifecycle. Any of the following 51 * lifecycle events can happen independently in a separate binding/creation of the 52 * service.</p> 53 * 54 * <ul> 55 * <li>When a tile is added by the user its TileService will be bound to and 56 * {@link #onTileAdded()} will be called.</li> 57 * 58 * <li>When a tile should be up to date and listing will be indicated by 59 * {@link #onStartListening()} and {@link #onStopListening()}.</li> 60 * 61 * <li>When the user removes a tile from Quick Settings {@link #onTileRemoved()} 62 * will be called.</li> 63 * 64 * <li>{@link #onTileAdded()} and {@link #onTileRemoved()} may be called outside of the 65 * {@link #onCreate()} - {@link #onDestroy()} window</li> 66 * </ul> 67 * <p>TileService will be detected by tiles that match the {@value #ACTION_QS_TILE} 68 * and require the permission "android.permission.BIND_QUICK_SETTINGS_TILE". 69 * The label and icon for the service will be used as the default label and 70 * icon for the tile. Here is an example TileService declaration.</p> 71 * <pre class="prettyprint"> 72 * {@literal 73 * <service 74 * android:name=".MyQSTileService" 75 * android:label="@string/my_default_tile_label" 76 * android:icon="@drawable/my_default_icon_label" 77 * android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> 78 * <intent-filter> 79 * <action android:name="android.service.quicksettings.action.QS_TILE" /> 80 * </intent-filter> 81 * </service>} 82 * </pre> 83 * 84 * @see Tile Tile for details about the UI of a Quick Settings Tile. 85 */ 86 public class TileService extends Service { 87 88 private static final String TAG = "TileService"; 89 private static final boolean DEBUG = false; 90 91 /** 92 * An activity that provides a user interface for adjusting TileService 93 * preferences. Optional but recommended for apps that implement a 94 * TileService. 95 * <p> 96 * This intent may also define a {@link Intent#EXTRA_COMPONENT_NAME} value 97 * to indicate the {@link ComponentName} that caused the preferences to be 98 * opened. 99 * <p> 100 * To ensure that the activity can only be launched through quick settings 101 * UI provided by this service, apps can protect it with the 102 * BIND_QUICK_SETTINGS_TILE permission. 103 */ 104 @SdkConstant(SdkConstantType.INTENT_CATEGORY) 105 public static final String ACTION_QS_TILE_PREFERENCES 106 = "android.service.quicksettings.action.QS_TILE_PREFERENCES"; 107 108 /** 109 * Action that identifies a Service as being a TileService. 110 */ 111 public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE"; 112 113 /** 114 * Meta-data for tile definition to set a tile into active mode. 115 * <p> 116 * Active mode is for tiles which already listen and keep track of their state in their 117 * own process. These tiles may request to send an update to the System while their process 118 * is alive using {@link #requestListeningState}. The System will only bind these tiles 119 * on its own when a click needs to occur. 120 * 121 * To make a TileService an active tile, set this meta-data to true on the TileService's 122 * manifest declaration. 123 * <pre class="prettyprint"> 124 * {@literal 125 * <meta-data android:name="android.service.quicksettings.ACTIVE_TILE" 126 * android:value="true" /> 127 * } 128 * </pre> 129 */ 130 public static final String META_DATA_ACTIVE_TILE 131 = "android.service.quicksettings.ACTIVE_TILE"; 132 133 /** 134 * Meta-data for a tile to mark is toggleable. 135 * <p> 136 * Toggleable tiles support switch tile behavior in accessibility. This is 137 * the behavior of most of the framework tiles. 138 * 139 * To indicate that a TileService is toggleable, set this meta-data to true on the 140 * TileService's manifest declaration. 141 * <pre class="prettyprint"> 142 * {@literal 143 * <meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" 144 * android:value="true" /> 145 * } 146 * </pre> 147 */ 148 public static final String META_DATA_TOGGLEABLE_TILE = 149 "android.service.quicksettings.TOGGLEABLE_TILE"; 150 151 /** 152 * @hide 153 */ 154 public static final String EXTRA_SERVICE = "service"; 155 156 /** 157 * @hide 158 */ 159 public static final String EXTRA_TOKEN = "token"; 160 161 /** 162 * @hide 163 */ 164 public static final String EXTRA_STATE = "state"; 165 166 private final H mHandler = new H(Looper.getMainLooper()); 167 168 private boolean mListening = false; 169 private Tile mTile; 170 private IBinder mToken; 171 private IQSService mService; 172 private Runnable mUnlockRunnable; 173 private IBinder mTileToken; 174 175 @Override onDestroy()176 public void onDestroy() { 177 if (mListening) { 178 onStopListening(); 179 mListening = false; 180 } 181 super.onDestroy(); 182 } 183 184 /** 185 * Called when the user adds this tile to Quick Settings. 186 * <p/> 187 * Note that this is not guaranteed to be called between {@link #onCreate()} 188 * and {@link #onStartListening()}, it will only be called when the tile is added 189 * and not on subsequent binds. 190 */ onTileAdded()191 public void onTileAdded() { 192 } 193 194 /** 195 * Called when the user removes this tile from Quick Settings. 196 */ onTileRemoved()197 public void onTileRemoved() { 198 } 199 200 /** 201 * Called when this tile moves into a listening state. 202 * <p/> 203 * When this tile is in a listening state it is expected to keep the 204 * UI up to date. Any listeners or callbacks needed to keep this tile 205 * up to date should be registered here and unregistered in {@link #onStopListening()}. 206 * 207 * @see #getQsTile() 208 * @see Tile#updateTile() 209 */ onStartListening()210 public void onStartListening() { 211 } 212 213 /** 214 * Called when this tile moves out of the listening state. 215 */ onStopListening()216 public void onStopListening() { 217 } 218 219 /** 220 * Called when the user clicks on this tile. 221 */ onClick()222 public void onClick() { 223 } 224 225 /** 226 * Sets an icon to be shown in the status bar. 227 * <p> 228 * The icon will be displayed before all other icons. Can only be called between 229 * {@link #onStartListening} and {@link #onStopListening}. Can only be called by system apps. 230 * 231 * @param icon The icon to be displayed, null to hide 232 * @param contentDescription Content description of the icon to be displayed 233 * @hide 234 */ 235 @SystemApi setStatusIcon(Icon icon, String contentDescription)236 public final void setStatusIcon(Icon icon, String contentDescription) { 237 if (mService != null) { 238 try { 239 mService.updateStatusIcon(mTileToken, icon, contentDescription); 240 } catch (RemoteException e) { 241 } 242 } 243 } 244 245 /** 246 * Used to show a dialog. 247 * 248 * This will collapse the Quick Settings panel and show the dialog. 249 * 250 * @param dialog Dialog to show. 251 * 252 * @see #isLocked() 253 */ showDialog(Dialog dialog)254 public final void showDialog(Dialog dialog) { 255 dialog.getWindow().getAttributes().token = mToken; 256 dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_QS_DIALOG); 257 dialog.getWindow().getDecorView().addOnAttachStateChangeListener( 258 new OnAttachStateChangeListener() { 259 @Override 260 public void onViewAttachedToWindow(View v) { 261 } 262 263 @Override 264 public void onViewDetachedFromWindow(View v) { 265 try { 266 mService.onDialogHidden(mTileToken); 267 } catch (RemoteException e) { 268 } 269 } 270 }); 271 dialog.show(); 272 try { 273 mService.onShowDialog(mTileToken); 274 } catch (RemoteException e) { 275 } 276 } 277 278 /** 279 * Prompts the user to unlock the device before executing the Runnable. 280 * <p> 281 * The user will be prompted for their current security method if applicable 282 * and if successful, runnable will be executed. The Runnable will not be 283 * executed if the user fails to unlock the device or cancels the operation. 284 */ unlockAndRun(Runnable runnable)285 public final void unlockAndRun(Runnable runnable) { 286 mUnlockRunnable = runnable; 287 try { 288 mService.startUnlockAndRun(mTileToken); 289 } catch (RemoteException e) { 290 } 291 } 292 293 /** 294 * Checks if the device is in a secure state. 295 * 296 * TileServices should detect when the device is secure and change their behavior 297 * accordingly. 298 * 299 * @return true if the device is secure. 300 */ isSecure()301 public final boolean isSecure() { 302 try { 303 return mService.isSecure(); 304 } catch (RemoteException e) { 305 return true; 306 } 307 } 308 309 /** 310 * Checks if the lock screen is showing. 311 * 312 * When a device is locked, then {@link #showDialog} will not present a dialog, as it will 313 * be under the lock screen. If the behavior of the Tile is safe to do while locked, 314 * then the user should use {@link #startActivity} to launch an activity on top of the lock 315 * screen, otherwise the tile should use {@link #unlockAndRun(Runnable)} to give the 316 * user their security challenge. 317 * 318 * @return true if the device is locked. 319 */ isLocked()320 public final boolean isLocked() { 321 try { 322 return mService.isLocked(); 323 } catch (RemoteException e) { 324 return true; 325 } 326 } 327 328 /** 329 * Start an activity while collapsing the panel. 330 */ startActivityAndCollapse(Intent intent)331 public final void startActivityAndCollapse(Intent intent) { 332 startActivity(intent); 333 try { 334 mService.onStartActivity(mTileToken); 335 } catch (RemoteException e) { 336 } 337 } 338 339 /** 340 * Starts an {@link android.app.Activity}. 341 * Will collapse Quick Settings after launching. 342 * 343 * @param pendingIntent A PendingIntent for an Activity to be launched immediately. 344 * @hide 345 */ startActivityAndCollapse(PendingIntent pendingIntent)346 public void startActivityAndCollapse(PendingIntent pendingIntent) { 347 try { 348 mService.startActivity(mTileToken, pendingIntent); 349 } catch (RemoteException e) { 350 } 351 } 352 353 /** 354 * Gets the {@link Tile} for this service. 355 * <p/> 356 * This tile may be used to get or set the current state for this 357 * tile. This tile is only valid for updates between {@link #onStartListening()} 358 * and {@link #onStopListening()}. 359 */ getQsTile()360 public final Tile getQsTile() { 361 return mTile; 362 } 363 364 @Override onBind(Intent intent)365 public IBinder onBind(Intent intent) { 366 mService = IQSService.Stub.asInterface(intent.getIBinderExtra(EXTRA_SERVICE)); 367 mTileToken = intent.getIBinderExtra(EXTRA_TOKEN); 368 try { 369 mTile = mService.getTile(mTileToken); 370 } catch (RemoteException e) { 371 String name = TileService.this.getClass().getSimpleName(); 372 Log.w(TAG, name + " - Couldn't get tile from IQSService.", e); 373 // If we couldn't receive the tile, there's not much reason to continue as users won't 374 // be able to interact. Returning `null` will trigger an unbind in SystemUI and 375 // eventually we'll rebind when needed. This usually means that SystemUI crashed 376 // right after binding and therefore `mService` is outdated. 377 return null; 378 } 379 if (mTile != null) { 380 mTile.setService(mService, mTileToken); 381 mHandler.sendEmptyMessage(H.MSG_START_SUCCESS); 382 } 383 return new IQSTileService.Stub() { 384 @Override 385 public void onTileRemoved() throws RemoteException { 386 mHandler.sendEmptyMessage(H.MSG_TILE_REMOVED); 387 } 388 389 @Override 390 public void onTileAdded() throws RemoteException { 391 mHandler.sendEmptyMessage(H.MSG_TILE_ADDED); 392 } 393 394 @Override 395 public void onStopListening() throws RemoteException { 396 mHandler.sendEmptyMessage(H.MSG_STOP_LISTENING); 397 } 398 399 @Override 400 public void onStartListening() throws RemoteException { 401 mHandler.sendEmptyMessage(H.MSG_START_LISTENING); 402 } 403 404 @Override 405 public void onClick(IBinder wtoken) throws RemoteException { 406 mHandler.obtainMessage(H.MSG_TILE_CLICKED, wtoken).sendToTarget(); 407 } 408 409 @Override 410 public void onUnlockComplete() throws RemoteException{ 411 mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE); 412 } 413 }; 414 } 415 416 private class H extends Handler { 417 private static final int MSG_START_LISTENING = 1; 418 private static final int MSG_STOP_LISTENING = 2; 419 private static final int MSG_TILE_ADDED = 3; 420 private static final int MSG_TILE_REMOVED = 4; 421 private static final int MSG_TILE_CLICKED = 5; 422 private static final int MSG_UNLOCK_COMPLETE = 6; 423 private static final int MSG_START_SUCCESS = 7; 424 private final String mTileServiceName; 425 426 public H(Looper looper) { 427 super(looper); 428 mTileServiceName = TileService.this.getClass().getSimpleName(); 429 } 430 431 private void logMessage(String message) { 432 Log.d(TAG, mTileServiceName + " Handler - " + message); 433 } 434 435 @Override 436 public void handleMessage(Message msg) { 437 switch (msg.what) { 438 case MSG_TILE_ADDED: 439 if (DEBUG) logMessage("MSG_TILE_ADDED"); 440 TileService.this.onTileAdded(); 441 break; 442 case MSG_TILE_REMOVED: 443 if (DEBUG) logMessage("MSG_TILE_REMOVED"); 444 if (mListening) { 445 mListening = false; 446 TileService.this.onStopListening(); 447 } 448 TileService.this.onTileRemoved(); 449 break; 450 case MSG_STOP_LISTENING: 451 if (DEBUG) logMessage("MSG_STOP_LISTENING"); 452 if (mListening) { 453 mListening = false; 454 TileService.this.onStopListening(); 455 } 456 break; 457 case MSG_START_LISTENING: 458 if (DEBUG) logMessage("MSG_START_LISTENING"); 459 if (!mListening) { 460 mListening = true; 461 TileService.this.onStartListening(); 462 } 463 break; 464 case MSG_TILE_CLICKED: 465 if (DEBUG) logMessage("MSG_TILE_CLICKED"); 466 mToken = (IBinder) msg.obj; 467 TileService.this.onClick(); 468 break; 469 case MSG_UNLOCK_COMPLETE: 470 if (DEBUG) logMessage("MSG_UNLOCK_COMPLETE"); 471 if (mUnlockRunnable != null) { 472 mUnlockRunnable.run(); 473 } 474 break; 475 case MSG_START_SUCCESS: 476 if (DEBUG) logMessage("MSG_START_SUCCESS"); 477 try { 478 mService.onStartSuccessful(mTileToken); 479 } catch (RemoteException e) { 480 } 481 break; 482 } 483 } 484 } 485 486 /** 487 * @return True if the device supports quick settings and its assocated APIs. 488 * @hide 489 */ 490 @TestApi 491 public static boolean isQuickSettingsSupported() { 492 return Resources.getSystem().getBoolean(R.bool.config_quickSettingsSupported); 493 } 494 495 /** 496 * Requests that a tile be put in the listening state so it can send an update. 497 * 498 * This method is only applicable to tiles that have {@link #META_DATA_ACTIVE_TILE} defined 499 * as true on their TileService Manifest declaration, and will do nothing otherwise. 500 * 501 * For apps targeting {@link Build.VERSION_CODES#TIRAMISU} or later, this call may throw 502 * the following exceptions if the request is not valid: 503 * <ul> 504 * <li> {@link NullPointerException} if {@code component} is {@code null}.</li> 505 * <li> {@link SecurityException} if the package of {@code component} does not match 506 * the calling package or if the calling user cannot act on behalf of the user from the 507 * {@code context}.</li> 508 * <li> {@link IllegalArgumentException} if the user of the {@code context} is not the 509 * current user. Only thrown for apps targeting {@link Build.VERSION_CODES#TIRAMISU}</li> 510 * </ul> 511 */ 512 public static final void requestListeningState(Context context, ComponentName component) { 513 StatusBarManager sbm = context.getSystemService(StatusBarManager.class); 514 if (sbm == null) { 515 Log.e(TAG, "No StatusBarManager service found"); 516 return; 517 } 518 sbm.requestTileServiceListeningState(component); 519 } 520 } 521