blob: f74f5332e8d362785cf5abde15ed5fb1d95f2d3b [file] [log] [blame]
Aurimas Liutikas88c7ff12023-08-10 12:42:26 -07001/*
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 */
16package android.service.quicksettings;
17
18import android.annotation.NonNull;
19import android.annotation.SdkConstant;
20import android.annotation.SdkConstant.SdkConstantType;
21import android.annotation.SystemApi;
22import android.annotation.TestApi;
23import android.app.Dialog;
24import android.app.PendingIntent;
25import android.app.Service;
26import android.app.StatusBarManager;
27import android.app.compat.CompatChanges;
28import android.compat.annotation.ChangeId;
29import android.compat.annotation.EnabledSince;
30import android.content.ComponentName;
31import android.content.Context;
32import android.content.Intent;
33import android.content.res.Resources;
34import android.graphics.drawable.Icon;
35import android.os.Build;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.Looper;
39import android.os.Message;
40import android.os.RemoteException;
41import android.util.Log;
42import android.view.View;
43import android.view.View.OnAttachStateChangeListener;
44import android.view.WindowManager;
45
46import com.android.internal.R;
47
48import 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 */
92public 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
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 */
208 public void onTileAdded() {
209 }
210
211 /**
212 * Called when the user removes this tile from Quick Settings.
213 */
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 */
227 public void onStartListening() {
228 }
229
230 /**
231 * Called when this tile moves out of the listening state.
232 */
233 public void onStopListening() {
234 }
235
236 /**
237 * Called when the user clicks on this tile.
238 */
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
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 */
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 */
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 */
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 */
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
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 */
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 */
387 public final Tile getQsTile() {
388 return mTile;
389 }
390
391 @Override
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}