/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.quicksettings; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.Dialog; import android.app.PendingIntent; import android.app.Service; import android.app.StatusBarManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.WindowManager; import com.android.internal.R; /** * A TileService provides the user a tile that can be added to Quick Settings. * Quick Settings is a space provided that allows the user to change settings and * take quick actions without leaving the context of their current app. * *
The lifecycle of a TileService is different from some other services in * that it may be unbound during parts of its lifecycle. Any of the following * lifecycle events can happen independently in a separate binding/creation of the * service.
* *TileService will be detected by tiles that match the {@value #ACTION_QS_TILE} * and require the permission "android.permission.BIND_QUICK_SETTINGS_TILE". * The label and icon for the service will be used as the default label and * icon for the tile. Here is an example TileService declaration.
*
* {@literal
*
*
*
*
* }
*
*
* @see Tile Tile for details about the UI of a Quick Settings Tile.
*/
public class TileService extends Service {
private static final String TAG = "TileService";
private static final boolean DEBUG = false;
/**
* An activity that provides a user interface for adjusting TileService
* preferences. Optional but recommended for apps that implement a
* TileService.
* * This intent may also define a {@link Intent#EXTRA_COMPONENT_NAME} value * to indicate the {@link ComponentName} that caused the preferences to be * opened. *
* To ensure that the activity can only be launched through quick settings * UI provided by this service, apps can protect it with the * BIND_QUICK_SETTINGS_TILE permission. */ @SdkConstant(SdkConstantType.INTENT_CATEGORY) public static final String ACTION_QS_TILE_PREFERENCES = "android.service.quicksettings.action.QS_TILE_PREFERENCES"; /** * Action that identifies a Service as being a TileService. */ public static final String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE"; /** * Meta-data for tile definition to set a tile into active mode. *
* Active mode is for tiles which already listen and keep track of their state in their * own process. These tiles may request to send an update to the System while their process * is alive using {@link #requestListeningState}. The System will only bind these tiles * on its own when a click needs to occur. * * To make a TileService an active tile, set this meta-data to true on the TileService's * manifest declaration. *
* {@literal
*
* }
*
*/
public static final String META_DATA_ACTIVE_TILE
= "android.service.quicksettings.ACTIVE_TILE";
/**
* Meta-data for a tile to mark is toggleable.
* * Toggleable tiles support switch tile behavior in accessibility. This is * the behavior of most of the framework tiles. * * To indicate that a TileService is toggleable, set this meta-data to true on the * TileService's manifest declaration. *
* {@literal
*
* }
*
*/
public static final String META_DATA_TOGGLEABLE_TILE =
"android.service.quicksettings.TOGGLEABLE_TILE";
/**
* @hide
*/
public static final String EXTRA_SERVICE = "service";
/**
* @hide
*/
public static final String EXTRA_TOKEN = "token";
/**
* @hide
*/
public static final String EXTRA_STATE = "state";
private final H mHandler = new H(Looper.getMainLooper());
private boolean mListening = false;
private Tile mTile;
private IBinder mToken;
private IQSService mService;
private Runnable mUnlockRunnable;
private IBinder mTileToken;
@Override
public void onDestroy() {
if (mListening) {
onStopListening();
mListening = false;
}
super.onDestroy();
}
/**
* Called when the user adds this tile to Quick Settings.
*
* Note that this is not guaranteed to be called between {@link #onCreate()}
* and {@link #onStartListening()}, it will only be called when the tile is added
* and not on subsequent binds.
*/
public void onTileAdded() {
}
/**
* Called when the user removes this tile from Quick Settings.
*/
public void onTileRemoved() {
}
/**
* Called when this tile moves into a listening state.
*
* When this tile is in a listening state it is expected to keep the
* UI up to date. Any listeners or callbacks needed to keep this tile
* up to date should be registered here and unregistered in {@link #onStopListening()}.
*
* @see #getQsTile()
* @see Tile#updateTile()
*/
public void onStartListening() {
}
/**
* Called when this tile moves out of the listening state.
*/
public void onStopListening() {
}
/**
* Called when the user clicks on this tile.
*/
public void onClick() {
}
/**
* Sets an icon to be shown in the status bar.
* * The icon will be displayed before all other icons. Can only be called between * {@link #onStartListening} and {@link #onStopListening}. Can only be called by system apps. * * @param icon The icon to be displayed, null to hide * @param contentDescription Content description of the icon to be displayed * @hide */ @SystemApi public final void setStatusIcon(Icon icon, String contentDescription) { if (mService != null) { try { mService.updateStatusIcon(mTileToken, icon, contentDescription); } catch (RemoteException e) { } } } /** * Used to show a dialog. * * This will collapse the Quick Settings panel and show the dialog. * * @param dialog Dialog to show. * * @see #isLocked() */ public final void showDialog(Dialog dialog) { dialog.getWindow().getAttributes().token = mToken; dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_QS_DIALOG); dialog.getWindow().getDecorView().addOnAttachStateChangeListener( new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { try { mService.onDialogHidden(mTileToken); } catch (RemoteException e) { } } }); dialog.show(); try { mService.onShowDialog(mTileToken); } catch (RemoteException e) { } } /** * Prompts the user to unlock the device before executing the Runnable. *
* The user will be prompted for their current security method if applicable * and if successful, runnable will be executed. The Runnable will not be * executed if the user fails to unlock the device or cancels the operation. */ public final void unlockAndRun(Runnable runnable) { mUnlockRunnable = runnable; try { mService.startUnlockAndRun(mTileToken); } catch (RemoteException e) { } } /** * Checks if the device is in a secure state. * * TileServices should detect when the device is secure and change their behavior * accordingly. * * @return true if the device is secure. */ public final boolean isSecure() { try { return mService.isSecure(); } catch (RemoteException e) { return true; } } /** * Checks if the lock screen is showing. * * When a device is locked, then {@link #showDialog} will not present a dialog, as it will * be under the lock screen. If the behavior of the Tile is safe to do while locked, * then the user should use {@link #startActivity} to launch an activity on top of the lock * screen, otherwise the tile should use {@link #unlockAndRun(Runnable)} to give the * user their security challenge. * * @return true if the device is locked. */ public final boolean isLocked() { try { return mService.isLocked(); } catch (RemoteException e) { return true; } } /** * Start an activity while collapsing the panel. */ public final void startActivityAndCollapse(Intent intent) { startActivity(intent); try { mService.onStartActivity(mTileToken); } catch (RemoteException e) { } } /** * Starts an {@link android.app.Activity}. * Will collapse Quick Settings after launching. * * @param pendingIntent A PendingIntent for an Activity to be launched immediately. * @hide */ public void startActivityAndCollapse(PendingIntent pendingIntent) { try { mService.startActivity(mTileToken, pendingIntent); } catch (RemoteException e) { } } /** * Gets the {@link Tile} for this service. *
* This tile may be used to get or set the current state for this * tile. This tile is only valid for updates between {@link #onStartListening()} * and {@link #onStopListening()}. */ public final Tile getQsTile() { return mTile; } @Override public IBinder onBind(Intent intent) { mService = IQSService.Stub.asInterface(intent.getIBinderExtra(EXTRA_SERVICE)); mTileToken = intent.getIBinderExtra(EXTRA_TOKEN); try { mTile = mService.getTile(mTileToken); } catch (RemoteException e) { String name = TileService.this.getClass().getSimpleName(); Log.w(TAG, name + " - Couldn't get tile from IQSService.", e); // If we couldn't receive the tile, there's not much reason to continue as users won't // be able to interact. Returning `null` will trigger an unbind in SystemUI and // eventually we'll rebind when needed. This usually means that SystemUI crashed // right after binding and therefore `mService` is outdated. return null; } if (mTile != null) { mTile.setService(mService, mTileToken); mHandler.sendEmptyMessage(H.MSG_START_SUCCESS); } return new IQSTileService.Stub() { @Override public void onTileRemoved() throws RemoteException { mHandler.sendEmptyMessage(H.MSG_TILE_REMOVED); } @Override public void onTileAdded() throws RemoteException { mHandler.sendEmptyMessage(H.MSG_TILE_ADDED); } @Override public void onStopListening() throws RemoteException { mHandler.sendEmptyMessage(H.MSG_STOP_LISTENING); } @Override public void onStartListening() throws RemoteException { mHandler.sendEmptyMessage(H.MSG_START_LISTENING); } @Override public void onClick(IBinder wtoken) throws RemoteException { mHandler.obtainMessage(H.MSG_TILE_CLICKED, wtoken).sendToTarget(); } @Override public void onUnlockComplete() throws RemoteException{ mHandler.sendEmptyMessage(H.MSG_UNLOCK_COMPLETE); } }; } private class H extends Handler { private static final int MSG_START_LISTENING = 1; private static final int MSG_STOP_LISTENING = 2; private static final int MSG_TILE_ADDED = 3; private static final int MSG_TILE_REMOVED = 4; private static final int MSG_TILE_CLICKED = 5; private static final int MSG_UNLOCK_COMPLETE = 6; private static final int MSG_START_SUCCESS = 7; private final String mTileServiceName; public H(Looper looper) { super(looper); mTileServiceName = TileService.this.getClass().getSimpleName(); } private void logMessage(String message) { Log.d(TAG, mTileServiceName + " Handler - " + message); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_TILE_ADDED: if (DEBUG) logMessage("MSG_TILE_ADDED"); TileService.this.onTileAdded(); break; case MSG_TILE_REMOVED: if (DEBUG) logMessage("MSG_TILE_REMOVED"); if (mListening) { mListening = false; TileService.this.onStopListening(); } TileService.this.onTileRemoved(); break; case MSG_STOP_LISTENING: if (DEBUG) logMessage("MSG_STOP_LISTENING"); if (mListening) { mListening = false; TileService.this.onStopListening(); } break; case MSG_START_LISTENING: if (DEBUG) logMessage("MSG_START_LISTENING"); if (!mListening) { mListening = true; TileService.this.onStartListening(); } break; case MSG_TILE_CLICKED: if (DEBUG) logMessage("MSG_TILE_CLICKED"); mToken = (IBinder) msg.obj; TileService.this.onClick(); break; case MSG_UNLOCK_COMPLETE: if (DEBUG) logMessage("MSG_UNLOCK_COMPLETE"); if (mUnlockRunnable != null) { mUnlockRunnable.run(); } break; case MSG_START_SUCCESS: if (DEBUG) logMessage("MSG_START_SUCCESS"); try { mService.onStartSuccessful(mTileToken); } catch (RemoteException e) { } break; } } } /** * @return True if the device supports quick settings and its assocated APIs. * @hide */ @TestApi public static boolean isQuickSettingsSupported() { return Resources.getSystem().getBoolean(R.bool.config_quickSettingsSupported); } /** * Requests that a tile be put in the listening state so it can send an update. * * This method is only applicable to tiles that have {@link #META_DATA_ACTIVE_TILE} defined * as true on their TileService Manifest declaration, and will do nothing otherwise. * * For apps targeting {@link Build.VERSION_CODES#TIRAMISU} or later, this call may throw * the following exceptions if the request is not valid: *