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