From 9429513cc3ea6e58e330865bd621b57cb3477551 Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Tue, 12 Jan 2016 11:27:02 -0500 Subject: [PATCH] Round out the QS API - Add startActivityAndCollapse, to make collapsing the shade easy - Add isSecure() - Add isLocked() - Add unlockandRun(Runnable) - Add unavailable, active, and inactive states The states are added to allow consistent UI across OEM devices, by allowing UI tweaking and tinting to match system tiles with custom ones. The combination of isSecure() and isLocked() and unlockAndRun(Runnable) allows all combinations of launching show when lockend and triggering an unlock when needed for sensitive tiles. Change-Id: Iade98ad9f2c22aa174e62090d8ccd44c86f3bb3c --- api/current.txt | 9 +++ api/system-current.txt | 9 +++ api/test-current.txt | 9 +++ .../android/service/quicksettings/IQSService.aidl | 4 ++ .../service/quicksettings/IQSTileService.aidl | 1 + core/java/android/service/quicksettings/Tile.java | 52 +++++++++++++++ .../android/service/quicksettings/TileService.java | 75 ++++++++++++++++++++++ packages/SystemUI/res/values/colors.xml | 4 ++ .../systemui/qs/customize/BlankCustomTile.java | 7 +- .../android/systemui/qs/external/CustomTile.java | 39 ++++++++++- .../systemui/qs/external/QSTileServiceWrapper.java | 10 +++ .../systemui/qs/external/TileLifecycleManager.java | 19 ++++++ .../android/systemui/qs/external/TileServices.java | 33 ++++++++++ .../qs/external/TileLifecycleManagerTests.java | 5 ++ 14 files changed, 273 insertions(+), 3 deletions(-) diff --git a/api/current.txt b/api/current.txt index cfb688f0198f..e5793ad95c3c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -33724,17 +33724,24 @@ package android.service.quicksettings { method public java.lang.CharSequence getContentDescription(); method public android.graphics.drawable.Icon getIcon(); method public java.lang.CharSequence getLabel(); + method public int getState(); method public void setContentDescription(java.lang.CharSequence); method public void setIcon(android.graphics.drawable.Icon); method public void setLabel(java.lang.CharSequence); + method public void setState(int); method public void updateTile(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int STATE_ACTIVE = 2; // 0x2 + field public static final int STATE_INACTIVE = 1; // 0x1 + field public static final int STATE_UNAVAILABLE = 0; // 0x0 } public class TileService extends android.app.Service { ctor public TileService(); method public final android.service.quicksettings.Tile getQsTile(); + method public final boolean isLocked(); + method public final boolean isSecure(); method public android.os.IBinder onBind(android.content.Intent); method public void onClick(); method public void onStartListening(); @@ -33743,6 +33750,8 @@ package android.service.quicksettings { method public void onTileRemoved(); method public static final void requestListeningState(android.content.Context, android.content.ComponentName); method public final void showDialog(android.app.Dialog); + method public final void startActivityAndCollapse(android.content.Intent); + method public final void unlockAndRun(java.lang.Runnable); field public static final java.lang.String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE"; field public static final int TILE_MODE_ACTIVE = 2; // 0x2 field public static final int TILE_MODE_PASSIVE = 1; // 0x1 diff --git a/api/system-current.txt b/api/system-current.txt index 9da472e287ab..3c4ede62e2f8 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -35893,17 +35893,24 @@ package android.service.quicksettings { method public java.lang.CharSequence getContentDescription(); method public android.graphics.drawable.Icon getIcon(); method public java.lang.CharSequence getLabel(); + method public int getState(); method public void setContentDescription(java.lang.CharSequence); method public void setIcon(android.graphics.drawable.Icon); method public void setLabel(java.lang.CharSequence); + method public void setState(int); method public void updateTile(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int STATE_ACTIVE = 2; // 0x2 + field public static final int STATE_INACTIVE = 1; // 0x1 + field public static final int STATE_UNAVAILABLE = 0; // 0x0 } public class TileService extends android.app.Service { ctor public TileService(); method public final android.service.quicksettings.Tile getQsTile(); + method public final boolean isLocked(); + method public final boolean isSecure(); method public android.os.IBinder onBind(android.content.Intent); method public void onClick(); method public void onStartListening(); @@ -35913,6 +35920,8 @@ package android.service.quicksettings { method public static final void requestListeningState(android.content.Context, android.content.ComponentName); method public final void setStatusIcon(android.graphics.drawable.Icon, java.lang.String); method public final void showDialog(android.app.Dialog); + method public final void startActivityAndCollapse(android.content.Intent); + method public final void unlockAndRun(java.lang.Runnable); field public static final java.lang.String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE"; field public static final int TILE_MODE_ACTIVE = 2; // 0x2 field public static final int TILE_MODE_PASSIVE = 1; // 0x1 diff --git a/api/test-current.txt b/api/test-current.txt index 9b9f83d82869..e2e978414fbd 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -33738,17 +33738,24 @@ package android.service.quicksettings { method public java.lang.CharSequence getContentDescription(); method public android.graphics.drawable.Icon getIcon(); method public java.lang.CharSequence getLabel(); + method public int getState(); method public void setContentDescription(java.lang.CharSequence); method public void setIcon(android.graphics.drawable.Icon); method public void setLabel(java.lang.CharSequence); + method public void setState(int); method public void updateTile(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; + field public static final int STATE_ACTIVE = 2; // 0x2 + field public static final int STATE_INACTIVE = 1; // 0x1 + field public static final int STATE_UNAVAILABLE = 0; // 0x0 } public class TileService extends android.app.Service { ctor public TileService(); method public final android.service.quicksettings.Tile getQsTile(); + method public final boolean isLocked(); + method public final boolean isSecure(); method public android.os.IBinder onBind(android.content.Intent); method public void onClick(); method public void onStartListening(); @@ -33757,6 +33764,8 @@ package android.service.quicksettings { method public void onTileRemoved(); method public static final void requestListeningState(android.content.Context, android.content.ComponentName); method public final void showDialog(android.app.Dialog); + method public final void startActivityAndCollapse(android.content.Intent); + method public final void unlockAndRun(java.lang.Runnable); field public static final java.lang.String ACTION_QS_TILE = "android.service.quicksettings.action.QS_TILE"; field public static final int TILE_MODE_ACTIVE = 2; // 0x2 field public static final int TILE_MODE_PASSIVE = 1; // 0x1 diff --git a/core/java/android/service/quicksettings/IQSService.aidl b/core/java/android/service/quicksettings/IQSService.aidl index 9991d416bb5a..4bfc94898f52 100644 --- a/core/java/android/service/quicksettings/IQSService.aidl +++ b/core/java/android/service/quicksettings/IQSService.aidl @@ -27,5 +27,9 @@ interface IQSService { void updateStatusIcon(in Tile tile, in Icon icon, String contentDescription); void onShowDialog(in Tile tile); + void onStartActivity(in Tile tile); void setTileMode(in ComponentName component, int mode); + boolean isLocked(); + boolean isSecure(); + void startUnlockAndRun(in Tile tile); } diff --git a/core/java/android/service/quicksettings/IQSTileService.aidl b/core/java/android/service/quicksettings/IQSTileService.aidl index 4997f754d2a7..bfde8702338a 100644 --- a/core/java/android/service/quicksettings/IQSTileService.aidl +++ b/core/java/android/service/quicksettings/IQSTileService.aidl @@ -29,4 +29,5 @@ oneway interface IQSTileService { void onStartListening(); void onStopListening(); void onClick(IBinder wtoken); + void onUnlockComplete(); } diff --git a/core/java/android/service/quicksettings/Tile.java b/core/java/android/service/quicksettings/Tile.java index 6104913642f8..85f1955e8ebd 100644 --- a/core/java/android/service/quicksettings/Tile.java +++ b/core/java/android/service/quicksettings/Tile.java @@ -36,10 +36,37 @@ public final class Tile implements Parcelable { private static final String TAG = "Tile"; + /** + * This is the default state of any tile, until updated by the {@link TileService}. + *

+ * An unavailable state indicates that for some reason this tile is not currently + * available to the user for some reason, and will have no click action. The tile's + * icon will be tinted differently to reflect this state. + */ + public static final int STATE_UNAVAILABLE = 0; + + /** + * This represents a tile that is currently in a disabled state but is still interactable. + * + * A disabled state indicates that the tile is not currently active (e.g. wifi disconnected or + * bluetooth disabled), but is still interactable by the user to modify this state. Tiles + * that have boolean states should use this to represent one of their states. The tile's + * icon will be tinted differently to reflect this state, but still be distinct from unavailable. + */ + public static final int STATE_INACTIVE = 1; + + /** + * This represents a tile that is currently active. (e.g. wifi is connected, bluetooth is on, + * cast is casting). + */ + public static final int STATE_ACTIVE = 2; + private ComponentName mComponentName; private Icon mIcon; private CharSequence mLabel; private CharSequence mContentDescription; + // Default to active until clients of the new API can update. + private int mState = STATE_ACTIVE; private IQSService mService; @@ -79,6 +106,29 @@ public final class Tile implements Parcelable { } /** + * The current state of the tile. + * + * @see #STATE_UNAVAILABLE + * @see #STATE_INACTIVE + * @see #STATE_ACTIVE + */ + public int getState() { + return mState; + } + + /** + * Sets the current state for the tile. + * + * Does not take effect until {@link #updateTile()} is called. + * + * @param state One of {@link #STATE_UNAVAILABLE}, {@link #STATE_INACTIVE}, + * {@link #STATE_ACTIVE} + */ + public void setState(int state) { + mState = state; + } + + /** * Gets the current icon for the tile. */ public Icon getIcon() { @@ -165,6 +215,7 @@ public final class Tile implements Parcelable { } else { dest.writeByte((byte) 0); } + dest.writeInt(mState); TextUtils.writeToParcel(mLabel, dest, flags); TextUtils.writeToParcel(mContentDescription, dest, flags); } @@ -180,6 +231,7 @@ public final class Tile implements Parcelable { } else { mIcon = null; } + mState = source.readInt(); mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); } diff --git a/core/java/android/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java index 6b1219335cb8..c02465b3f555 100644 --- a/core/java/android/service/quicksettings/TileService.java +++ b/core/java/android/service/quicksettings/TileService.java @@ -118,6 +118,7 @@ public class TileService extends Service { private Tile mTile; private IBinder mToken; private IQSService mService; + private Runnable mUnlockRunnable; @Override public void onDestroy() { @@ -199,6 +200,8 @@ public class TileService extends Service { * 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; @@ -211,6 +214,67 @@ public class TileService extends Service { } /** + * 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(mTile); + } 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(mTile); + } catch (RemoteException e) { + } + } + + /** * Gets the {@link Tile} for this service. *

* This tile may be used to get or set the current state for this @@ -258,6 +322,11 @@ public class TileService extends Service { 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); + } }; } @@ -269,6 +338,7 @@ public class TileService extends Service { private static final int MSG_TILE_REMOVED = 5; private static final int MSG_TILE_CLICKED = 6; private static final int MSG_SET_SERVICE = 7; + private static final int MSG_UNLOCK_COMPLETE = 8; public H(Looper looper) { super(looper); @@ -323,6 +393,11 @@ public class TileService extends Service { mToken = (IBinder) msg.obj; TileService.this.onClick(); break; + case MSG_UNLOCK_COMPLETE: + if (mUnlockRunnable != null) { + mUnlockRunnable.run(); + } + break; } } } diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 905da1388f4c..0a56f92e04cd 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -147,4 +147,8 @@ @*android:color/notification_default_color #4dffffff + + #40ffffff + #4dffffff + #ffffffff diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java index 95ff611d7419..ac4f05fa148c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/BlankCustomTile.java @@ -19,8 +19,9 @@ package com.android.systemui.qs.customize; import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; - +import android.graphics.drawable.Drawable; import com.android.internal.logging.MetricsLogger; +import com.android.systemui.R; import com.android.systemui.qs.QSTile; public class BlankCustomTile extends QSTile { @@ -72,7 +73,9 @@ public class BlankCustomTile extends QSTile { try { PackageManager pm = mContext.getPackageManager(); ServiceInfo info = pm.getServiceInfo(mComponent, 0); - state.icon = new DrawableIcon(info.loadIcon(pm)); + Drawable drawable = info.loadIcon(pm); + drawable.setTint(mContext.getColor(R.color.qs_tile_tint_active)); + state.icon = new DrawableIcon(drawable); state.label = info.loadLabel(pm).toString(); state.contentDescription = state.label; } catch (Exception e) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java index eefff30910a6..d398b644b9ac 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java @@ -25,11 +25,14 @@ import android.os.RemoteException; import android.service.quicksettings.IQSTileService; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; +import android.text.SpannableStringBuilder; +import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.IWindowManager; import android.view.WindowManager; import android.view.WindowManagerGlobal; import com.android.internal.logging.MetricsLogger; +import com.android.systemui.R; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.phone.QSTileHost; @@ -88,6 +91,7 @@ public class CustomTile extends QSTile { mTile.setIcon(tile.getIcon()); mTile.setLabel(tile.getLabel()); mTile.setContentDescription(tile.getContentDescription()); + mTile.setState(tile.getState()); } public void onDialogShown() { @@ -147,6 +151,9 @@ public class CustomTile extends QSTile { @Override protected void handleClick() { + if (mTile.getState() == Tile.STATE_UNAVAILABLE) { + return; + } try { if (DEBUG) Log.d(TAG, "Adding token"); mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG); @@ -172,9 +179,15 @@ public class CustomTile extends QSTile { @Override protected void handleUpdateState(State state, Object arg) { Drawable drawable = mTile.getIcon().loadDrawable(mContext); - drawable.setTint(mContext.getColor(android.R.color.white)); + int color = mContext.getColor(getColor(mTile.getState())); + drawable.setTint(color); state.icon = new DrawableIcon(drawable); state.label = mTile.getLabel(); + if (mTile.getState() == Tile.STATE_UNAVAILABLE) { + state.label = new SpannableStringBuilder().append(state.label, + new ForegroundColorSpan(color), + SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE); + } if (mTile.getContentDescription() != null) { state.contentDescription = mTile.getContentDescription(); } else { @@ -187,6 +200,30 @@ public class CustomTile extends QSTile { return MetricsLogger.QS_CUSTOM; } + public void startUnlockAndRun() { + mHost.startRunnableDismissingKeyguard(new Runnable() { + @Override + public void run() { + try { + mService.onUnlockComplete(); + } catch (RemoteException e) { + } + } + }); + } + + private static int getColor(int state) { + switch (state) { + case Tile.STATE_UNAVAILABLE: + return R.color.qs_tile_tint_unavailable; + case Tile.STATE_INACTIVE: + return R.color.qs_tile_tint_inactive; + case Tile.STATE_ACTIVE: + return R.color.qs_tile_tint_active; + } + return 0; + } + public static ComponentName getComponentFromSpec(String spec) { final String action = spec.substring(PREFIX.length(), spec.length() - 1); if (action.isEmpty()) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java b/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java index d41cdde37958..3830ac50c60b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java @@ -104,4 +104,14 @@ public class QSTileServiceWrapper { return false; } } + + public boolean onUnlockComplete() { + try { + mService.onUnlockComplete(); + return true; + } catch (Exception e) { + Log.d(TAG, "Caught exception from TileService", e); + return false; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java index 8c5e87e1a0f6..4977d80ee030 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java @@ -55,6 +55,7 @@ public class TileLifecycleManager extends BroadcastReceiver implements private static final int MSG_ON_ADDED = 0; private static final int MSG_ON_REMOVED = 1; private static final int MSG_ON_CLICK = 2; + private static final int MSG_ON_UNLOCK_COMPLETE = 3; // Bind retry control. private static final int MAX_BIND_RETRIES = 5; @@ -174,6 +175,15 @@ public class TileLifecycleManager extends BroadcastReceiver implements onClick(mClickBinder); } } + if (queue.contains(MSG_ON_UNLOCK_COMPLETE)) { + if (DEBUG) Log.d(TAG, "Handling pending onUnlockComplete"); + if (!mListening) { + Log.w(TAG, "Managed to get unlock on non-listening state..."); + // Skipping unlock since lost click privileges. + } else { + onUnlockComplete(); + } + } if (queue.contains(MSG_ON_REMOVED)) { if (DEBUG) Log.d(TAG, "Handling pending onRemoved"); if (mListening) { @@ -348,6 +358,15 @@ public class TileLifecycleManager extends BroadcastReceiver implements } @Override + public void onUnlockComplete() { + if (DEBUG) Log.d(TAG, "onUnlockComplete"); + if (mWrapper == null || !mWrapper.onUnlockComplete()) { + queueMessage(MSG_ON_UNLOCK_COMPLETE); + handleDeath(); + } + } + + @Override public IBinder asBinder() { return mWrapper != null ? mWrapper.asBinder() : null; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java index a831c87b9d86..44d8776bd44d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java @@ -36,6 +36,7 @@ import android.util.Log; import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.phone.StatusBarIconController; +import com.android.systemui.statusbar.policy.KeyguardMonitor; import java.util.ArrayList; import java.util.Collections; @@ -199,6 +200,16 @@ public class TileServices extends IQSService.Stub { } @Override + public void onStartActivity(Tile tile) { + ComponentName componentName = tile.getComponentName(); + verifyCaller(componentName.getPackageName()); + CustomTile customTile = getTileForComponent(componentName); + if (customTile != null) { + mHost.collapsePanels(); + } + } + + @Override public void updateStatusIcon(Tile tile, Icon icon, String contentDescription) { final ComponentName componentName = tile.getComponentName(); String packageName = componentName.getPackageName(); @@ -228,6 +239,28 @@ public class TileServices extends IQSService.Stub { } } + @Override + public void startUnlockAndRun(Tile tile) { + ComponentName componentName = tile.getComponentName(); + verifyCaller(componentName.getPackageName()); + CustomTile customTile = getTileForComponent(componentName); + if (customTile != null) { + customTile.startUnlockAndRun(); + } + } + + @Override + public boolean isLocked() { + KeyguardMonitor keyguardMonitor = mHost.getKeyguardMonitor(); + return keyguardMonitor.isShowing(); + } + + @Override + public boolean isSecure() { + KeyguardMonitor keyguardMonitor = mHost.getKeyguardMonitor(); + return keyguardMonitor.isSecure() && keyguardMonitor.isShowing(); + } + private CustomTile getTileForComponent(ComponentName component) { synchronized (mServices) { return mTiles.get(component); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTests.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTests.java index 6ebf488937f9..f86c6a46f1d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTests.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTests.java @@ -283,6 +283,11 @@ public class TileLifecycleManagerTests extends AndroidTestCase { public void onClick(IBinder iBinder) throws RemoteException { sendCallback("onClick"); } + + @Override + public void onUnlockComplete() throws RemoteException { + sendCallback("onUnlockComplete"); + } }; } -- 2.11.0