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