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