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