• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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