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