TileService.java revision 724214a87828ad0fe8a44db796a1f4e74eeae521
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 */
16package android.service.quicksettings;
17
18import android.Manifest;
19import android.annotation.SdkConstant;
20import android.annotation.SdkConstant.SdkConstantType;
21import android.annotation.SystemApi;
22import android.app.Dialog;
23import android.app.Service;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.graphics.drawable.Icon;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.Looper;
31import android.os.Message;
32import android.os.RemoteException;
33import android.view.View;
34import android.view.View.OnAttachStateChangeListener;
35import 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 */
76public 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     * The tile mode hasn't been set yet.
93     * @hide
94     */
95    public static final int TILE_MODE_UNSET = 0;
96
97    /**
98     * Constant to be returned by {@link #onTileAdded}.
99     * <p>
100     * Passive mode is the default mode for tiles.  The System will tell the tile
101     * when it is most important to update by putting it in the listening state.
102     */
103    public static final int TILE_MODE_PASSIVE = 1;
104
105    /**
106     * Constant to be returned by {@link #onTileAdded}.
107     * <p>
108     * Active mode is for tiles which already listen and keep track of their state in their
109     * own process.  These tiles may request to send an update to the System while their process
110     * is alive using {@link #requestListeningState}.  The System will only bind these tiles
111     * on its own when a click needs to occur.
112     */
113    public static final int TILE_MODE_ACTIVE = 2;
114
115    /**
116     * Used to notify SysUI that Listening has be requested.
117     * @hide
118     */
119    public static final String ACTION_REQUEST_LISTENING
120            = "android.service.quicksettings.action.REQUEST_LISTENING";
121
122    /**
123     * @hide
124     */
125    public static final String EXTRA_COMPONENT = "android.service.quicksettings.extra.COMPONENT";
126
127    private final H mHandler = new H(Looper.getMainLooper());
128
129    private boolean mListening = false;
130    private Tile mTile;
131    private IBinder mToken;
132    private IQSService mService;
133    private Runnable mUnlockRunnable;
134
135    @Override
136    public void onDestroy() {
137        if (mListening) {
138            onStopListening();
139            mListening = false;
140        }
141        super.onDestroy();
142    }
143
144    /**
145     * Called when the user adds this tile to Quick Settings.
146     * <p/>
147     * Note that this is not guaranteed to be called between {@link #onCreate()}
148     * and {@link #onStartListening()}, it will only be called when the tile is added
149     * and not on subsequent binds.
150     *
151     * @see #TILE_MODE_PASSIVE
152     * @see #TILE_MODE_ACTIVE
153     */
154    public int onTileAdded() {
155        return TILE_MODE_PASSIVE;
156    }
157
158    /**
159     * Called when the user removes this tile from Quick Settings.
160     */
161    public void onTileRemoved() {
162    }
163
164    /**
165     * Called when this tile moves into a listening state.
166     * <p/>
167     * When this tile is in a listening state it is expected to keep the
168     * UI up to date.  Any listeners or callbacks needed to keep this tile
169     * up to date should be registered here and unregistered in {@link #onStopListening()}.
170     *
171     * @see #getQsTile()
172     * @see Tile#updateTile()
173     */
174    public void onStartListening() {
175    }
176
177    /**
178     * Called when this tile moves out of the listening state.
179     */
180    public void onStopListening() {
181    }
182
183    /**
184     * Called when the user clicks on this tile.
185     */
186    public void onClick() {
187    }
188
189    /**
190     * Sets an icon to be shown in the status bar.
191     * <p>
192     * The icon will be displayed before all other icons.  Can only be called between
193     * {@link #onStartListening} and {@link #onStopListening}.  Can only be called by system apps.
194     *
195     * @param icon The icon to be displayed, null to hide
196     * @param contentDescription Content description of the icon to be displayed
197     * @hide
198     */
199    @SystemApi
200    public final void setStatusIcon(Icon icon, String contentDescription) {
201        if (mService != null) {
202            try {
203                mService.updateStatusIcon(mTile, icon, contentDescription);
204            } catch (RemoteException e) {
205            }
206        }
207    }
208
209    /**
210     * Used to show a dialog.
211     *
212     * This will collapse the Quick Settings panel and show the dialog.
213     *
214     * @param dialog Dialog to show.
215     *
216     * @see #isLocked()
217     */
218    public final void showDialog(Dialog dialog) {
219        dialog.getWindow().getAttributes().token = mToken;
220        dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_QS_DIALOG);
221        dialog.getWindow().getDecorView().addOnAttachStateChangeListener(
222                new OnAttachStateChangeListener() {
223            @Override
224            public void onViewAttachedToWindow(View v) {
225            }
226
227            @Override
228            public void onViewDetachedFromWindow(View v) {
229                try {
230                    mService.onDialogHidden(getQsTile());
231                } catch (RemoteException e) {
232                }
233            }
234        });
235        dialog.show();
236        try {
237            mService.onShowDialog(mTile);
238        } catch (RemoteException e) {
239        }
240    }
241
242    /**
243     * Prompts the user to unlock the device before executing the Runnable.
244     * <p>
245     * The user will be prompted for their current security method if applicable
246     * and if successful, runnable will be executed.  The Runnable will not be
247     * executed if the user fails to unlock the device or cancels the operation.
248     */
249    public final void unlockAndRun(Runnable runnable) {
250        mUnlockRunnable = runnable;
251        try {
252            mService.startUnlockAndRun(mTile);
253        } catch (RemoteException e) {
254        }
255    }
256
257    /**
258     * Checks if the device is in a secure state.
259     *
260     * TileServices should detect when the device is secure and change their behavior
261     * accordingly.
262     *
263     * @return true if the device is secure.
264     */
265    public final boolean isSecure() {
266        try {
267            return mService.isSecure();
268        } catch (RemoteException e) {
269            return true;
270        }
271    }
272
273    /**
274     * Checks if the lock screen is showing.
275     *
276     * When a device is locked, then {@link #showDialog} will not present a dialog, as it will
277     * be under the lock screen. If the behavior of the Tile is safe to do while locked,
278     * then the user should use {@link #startActivity} to launch an activity on top of the lock
279     * screen, otherwise the tile should use {@link #unlockAndRun(Runnable)} to give the
280     * user their security challenge.
281     *
282     * @return true if the device is locked.
283     */
284    public final boolean isLocked() {
285        try {
286            return mService.isLocked();
287        } catch (RemoteException e) {
288            return true;
289        }
290    }
291
292    /**
293     * Start an activity while collapsing the panel.
294     */
295    public final void startActivityAndCollapse(Intent intent) {
296        startActivity(intent);
297        try {
298            mService.onStartActivity(mTile);
299        } catch (RemoteException e) {
300        }
301    }
302
303    /**
304     * Gets the {@link Tile} for this service.
305     * <p/>
306     * This tile may be used to get or set the current state for this
307     * tile. This tile is only valid for updates between {@link #onStartListening()}
308     * and {@link #onStopListening()}.
309     */
310    public final Tile getQsTile() {
311        return mTile;
312    }
313
314    @Override
315    public IBinder onBind(Intent intent) {
316        return new IQSTileService.Stub() {
317            @Override
318            public void setQSService(IQSService service) throws RemoteException {
319                mHandler.obtainMessage(H.MSG_SET_SERVICE, service).sendToTarget();
320            }
321
322            @Override
323            public void setQSTile(Tile tile) throws RemoteException {
324                mHandler.obtainMessage(H.MSG_SET_TILE, tile).sendToTarget();
325            }
326
327            @Override
328            public void onTileRemoved() throws RemoteException {
329                mHandler.sendEmptyMessage(H.MSG_TILE_REMOVED);
330            }
331
332            @Override
333            public void onTileAdded() throws RemoteException {
334                mHandler.sendEmptyMessage(H.MSG_TILE_ADDED);
335            }
336
337            @Override
338            public void onStopListening() throws RemoteException {
339                mHandler.sendEmptyMessage(H.MSG_STOP_LISTENING);
340            }
341
342            @Override
343            public void onStartListening() throws RemoteException {
344                mHandler.sendEmptyMessage(H.MSG_START_LISTENING);
345            }
346
347            @Override
348            public void onClick(IBinder wtoken) throws RemoteException {
349                mHandler.obtainMessage(H.MSG_TILE_CLICKED, wtoken).sendToTarget();
350            }
351
352            @Override
353            public void onUnlockComplete() throws RemoteException{
354                mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE);
355            }
356        };
357    }
358
359    private class H extends Handler {
360        private static final int MSG_SET_TILE = 1;
361        private static final int MSG_START_LISTENING = 2;
362        private static final int MSG_STOP_LISTENING = 3;
363        private static final int MSG_TILE_ADDED = 4;
364        private static final int MSG_TILE_REMOVED = 5;
365        private static final int MSG_TILE_CLICKED = 6;
366        private static final int MSG_SET_SERVICE = 7;
367        private static final int MSG_UNLOCK_COMPLETE = 8;
368
369        public H(Looper looper) {
370            super(looper);
371        }
372
373        @Override
374        public void handleMessage(Message msg) {
375            switch (msg.what) {
376                case MSG_SET_SERVICE:
377                    mService = (IQSService) msg.obj;
378                    if (mTile != null) {
379                        mTile.setService(mService);
380                    }
381                    break;
382                case MSG_SET_TILE:
383                    mTile = (Tile) msg.obj;
384                    if (mService != null && mTile != null) {
385                        mTile.setService(mService);
386                    }
387                    break;
388                case MSG_TILE_ADDED:
389                    int mode = TileService.this.onTileAdded();
390                    if (mService == null) {
391                        return;
392                    }
393                    try {
394                        mService.setTileMode(new ComponentName(TileService.this,
395                                TileService.this.getClass()), mode);
396                    } catch (RemoteException e) {
397                    }
398                    break;
399                case MSG_TILE_REMOVED:
400                    if (mListening) {
401                        mListening = false;
402                        TileService.this.onStopListening();
403                    }
404                    TileService.this.onTileRemoved();
405                    break;
406                case MSG_STOP_LISTENING:
407                    if (mListening) {
408                        mListening = false;
409                        TileService.this.onStopListening();
410                    }
411                    break;
412                case MSG_START_LISTENING:
413                    if (!mListening) {
414                        mListening = true;
415                        TileService.this.onStartListening();
416                    }
417                    break;
418                case MSG_TILE_CLICKED:
419                    mToken = (IBinder) msg.obj;
420                    TileService.this.onClick();
421                    break;
422                case MSG_UNLOCK_COMPLETE:
423                    if (mUnlockRunnable != null) {
424                        mUnlockRunnable.run();
425                    }
426                    break;
427            }
428        }
429    }
430
431    /**
432     * Requests that a tile be put in the listening state so it can send an update.
433     *
434     * This method is only applicable to tiles that return {@link #TILE_MODE_ACTIVE} from
435     * {@link #onTileAdded()}, and will do nothing otherwise.
436     */
437    public static final void requestListeningState(Context context, ComponentName component) {
438        Intent intent = new Intent(ACTION_REQUEST_LISTENING);
439        intent.putExtra(EXTRA_COMPONENT, component);
440        context.sendBroadcast(intent, Manifest.permission.BIND_QUICK_SETTINGS_TILE);
441    }
442}
443