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