diff --git a/api/current.txt b/api/current.txt index 1dcd309b16cab..d3d82160edd63 100644 --- a/api/current.txt +++ b/api/current.txt @@ -33726,17 +33726,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(); @@ -33745,6 +33752,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 adce4806c6541..d1421e5a792ad 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -35896,17 +35896,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(); @@ -35916,6 +35923,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 3fed63215746a..62dc46a522f89 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -33740,17 +33740,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(); @@ -33759,6 +33766,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 9991d416bb5af..4bfc94898f52e 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 4997f754d2a78..bfde8702338a0 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 6104913642f8a..85f1955e8ebd6 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; @@ -78,6 +105,29 @@ public final class Tile implements Parcelable { return mService; } + /** + * 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. */ @@ -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 6b1219335cb85..c02465b3f5556 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; @@ -210,6 +213,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. *

@@ -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 905da1388f4c4..0a56f92e04cda 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 95ff611d7419f..ac4f05fa148cc 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 eefff30910a69..d398b644b9acb 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 d41cdde37958b..3830ac50c60b2 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 8c5e87e1a0f6d..4977d80ee030c 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) { @@ -347,6 +357,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 a831c87b9d865..44d8776bd44d4 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; @@ -198,6 +199,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(); @@ -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 6ebf488937f91..f86c6a46f1d63 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"); + } }; }