• 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.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