diff --git a/core/api/current.txt b/core/api/current.txt index 0e948db82cee9..cb0b9f995ef90 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -38374,6 +38374,7 @@ package android.service.controls { method @NonNull public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); method @Nullable public CharSequence getZone(); + method public boolean isAuthRequired(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; field public static final int STATUS_DISABLED = 4; // 0x4 @@ -38388,6 +38389,7 @@ package android.service.controls { ctor public Control.StatefulBuilder(@NonNull android.service.controls.Control); method @NonNull public android.service.controls.Control build(); method @NonNull public android.service.controls.Control.StatefulBuilder setAppIntent(@NonNull android.app.PendingIntent); + method @NonNull public android.service.controls.Control.StatefulBuilder setAuthRequired(boolean); method @NonNull public android.service.controls.Control.StatefulBuilder setControlId(@NonNull String); method @NonNull public android.service.controls.Control.StatefulBuilder setControlTemplate(@NonNull android.service.controls.templates.ControlTemplate); method @NonNull public android.service.controls.Control.StatefulBuilder setCustomColor(@Nullable android.content.res.ColorStateList); diff --git a/core/java/android/service/controls/Control.java b/core/java/android/service/controls/Control.java index 2868f1bf35471..3b757d6e3dd32 100644 --- a/core/java/android/service/controls/Control.java +++ b/core/java/android/service/controls/Control.java @@ -121,6 +121,7 @@ public final class Control implements Parcelable { private final @Status int mStatus; private final @NonNull ControlTemplate mControlTemplate; private final @NonNull CharSequence mStatusText; + private final boolean mAuthRequired; /** * @param controlId the unique persistent identifier for this object. @@ -137,6 +138,8 @@ public final class Control implements Parcelable { * @param status * @param controlTemplate * @param statusText + * @param authRequired true if the control can not be interacted with until the device is + * unlocked */ Control(@NonNull String controlId, @DeviceTypes.DeviceType int deviceType, @@ -149,7 +152,8 @@ public final class Control implements Parcelable { @Nullable ColorStateList customColor, @Status int status, @NonNull ControlTemplate controlTemplate, - @NonNull CharSequence statusText) { + @NonNull CharSequence statusText, + boolean authRequired) { Preconditions.checkNotNull(controlId); Preconditions.checkNotNull(title); Preconditions.checkNotNull(subtitle); @@ -180,6 +184,7 @@ public final class Control implements Parcelable { } mControlTemplate = controlTemplate; mStatusText = statusText; + mAuthRequired = authRequired; } /** @@ -219,6 +224,7 @@ public final class Control implements Parcelable { ControlTemplateWrapper wrapper = ControlTemplateWrapper.CREATOR.createFromParcel(in); mControlTemplate = wrapper.getWrappedTemplate(); mStatusText = in.readCharSequence(); + mAuthRequired = in.readBoolean(); } /** @@ -336,6 +342,13 @@ public final class Control implements Parcelable { return mStatusText; } + /** + * @return true if the control can not be interacted with until the device is unlocked + */ + public boolean isAuthRequired() { + return mAuthRequired; + } + @Override public int describeContents() { return 0; @@ -376,6 +389,7 @@ public final class Control implements Parcelable { dest.writeInt(mStatus); new ControlTemplateWrapper(mControlTemplate).writeToParcel(dest, flags); dest.writeCharSequence(mStatusText); + dest.writeBoolean(mAuthRequired); } public static final @NonNull Creator CREATOR = new Creator() { @@ -409,6 +423,7 @@ public final class Control implements Parcelable { *
  • Status: {@link Status#STATUS_UNKNOWN} *
  • Control template: {@link ControlTemplate#getNoTemplateObject} *
  • Status text: {@code ""} + *
  • Auth Required: {@code true} * */ @SuppressLint("MutableBareField") @@ -585,7 +600,8 @@ public final class Control implements Parcelable { mCustomColor, STATUS_UNKNOWN, ControlTemplate.NO_TEMPLATE, - ""); + "", + true /* authRequired */); } } @@ -607,6 +623,7 @@ public final class Control implements Parcelable { *
  • Status: {@link Status#STATUS_UNKNOWN} *
  • Control template: {@link ControlTemplate#getNoTemplateObject} *
  • Status text: {@code ""} + *
  • Auth Required: {@code true} * */ public static final class StatefulBuilder { @@ -623,6 +640,7 @@ public final class Control implements Parcelable { private @Status int mStatus = STATUS_UNKNOWN; private @NonNull ControlTemplate mControlTemplate = ControlTemplate.NO_TEMPLATE; private @NonNull CharSequence mStatusText = ""; + private boolean mAuthRequired = true; /** * @param controlId the identifier for the {@link Control}. @@ -655,6 +673,7 @@ public final class Control implements Parcelable { mStatus = control.mStatus; mControlTemplate = control.mControlTemplate; mStatusText = control.mStatusText; + mAuthRequired = control.mAuthRequired; } /** @@ -820,6 +839,17 @@ public final class Control implements Parcelable { return this; } + /** + * @param authRequired true if the control can not be interacted with until the device is + * unlocked + * @return {@code this} + */ + @NonNull + public StatefulBuilder setAuthRequired(boolean authRequired) { + mAuthRequired = authRequired; + return this; + } + /** * @return a valid {@link Control} */ @@ -836,7 +866,8 @@ public final class Control implements Parcelable { mCustomColor, mStatus, mControlTemplate, - mStatusText); + mStatusText, + mAuthRequired); } } } diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java index c5277ee0ed5e4..47b16a3518065 100644 --- a/core/java/android/service/controls/ControlsProviderService.java +++ b/core/java/android/service/controls/ControlsProviderService.java @@ -116,6 +116,9 @@ public abstract class ControlsProviderService extends Service { * Calls to {@link Subscriber#onComplete} will not be expected. Instead, wait for the call from * {@link Subscription#cancel} to indicate that updates are no longer required. It is expected * that controls provided by this publisher were created using {@link Control.StatefulBuilder}. + * + * By default, all controls require the device to be unlocked in order for the user to interact + * with it. This can be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}. */ @NonNull public abstract Publisher createPublisherFor(@NonNull List controlIds); @@ -127,6 +130,9 @@ public abstract class ControlsProviderService extends Service { * {@link ControlAction.ResponseResult}. The Integer should indicate whether the action * was received successfully, or if additional prompts should be presented to * the user. Any visual control updates should be sent via the Publisher. + + * By default, all invocations of this method will require the device be unlocked. This can + * be modified per Control by {@link Control.StatefulBuilder#setAuthRequired}. */ public abstract void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumer consumer); diff --git a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java index 63e464240630c..5fe17eeaac2d1 100644 --- a/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java +++ b/core/tests/coretests/src/android/service/controls/ControlProviderServiceTest.java @@ -347,6 +347,7 @@ public class ControlProviderServiceTest { && Objects.equals(c1.getCustomColor(), c2.getCustomColor()) && c1.getStatus() == c2.getStatus() && Objects.equals(c1.getControlTemplate(), c2.getControlTemplate()) + && Objects.equals(c1.isAuthRequired(), c2.isAuthRequired()) && Objects.equals(c1.getStatusText(), c2.getStatusText()); } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 6f30ac3901e15..a29f3e91f227a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -77,23 +77,37 @@ class ControlActionCoordinatorImpl @Inject constructor( override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) { controlsMetricsLogger.touch(cvh, isLocked) - bouncerOrRun(createAction(cvh.cws.ci.controlId, { - cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) - cvh.action(BooleanAction(templateId, !isChecked)) - }, true /* blockable */)) + bouncerOrRun( + createAction( + cvh.cws.ci.controlId, + { + cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + cvh.action(BooleanAction(templateId, !isChecked)) + }, + true /* blockable */ + ), + isAuthRequired(cvh) + ) } override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) { controlsMetricsLogger.touch(cvh, isLocked) val blockable = cvh.usePanel() - bouncerOrRun(createAction(cvh.cws.ci.controlId, { - cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) - if (cvh.usePanel()) { - showDetail(cvh, control.getAppIntent()) - } else { - cvh.action(CommandAction(templateId)) - } - }, blockable)) + bouncerOrRun( + createAction( + cvh.cws.ci.controlId, + { + cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + if (cvh.usePanel()) { + showDetail(cvh, control.getAppIntent()) + } else { + cvh.action(CommandAction(templateId)) + } + }, + blockable + ), + isAuthRequired(cvh) + ) } override fun drag(isEdge: Boolean) { @@ -106,20 +120,33 @@ class ControlActionCoordinatorImpl @Inject constructor( override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) { controlsMetricsLogger.drag(cvh, isLocked) - bouncerOrRun(createAction(cvh.cws.ci.controlId, { - cvh.action(FloatAction(templateId, newValue)) - }, false /* blockable */)) + bouncerOrRun( + createAction( + cvh.cws.ci.controlId, + { cvh.action(FloatAction(templateId, newValue)) }, + false /* blockable */ + ), + isAuthRequired(cvh) + ) } override fun longPress(cvh: ControlViewHolder) { controlsMetricsLogger.longPress(cvh, isLocked) - bouncerOrRun(createAction(cvh.cws.ci.controlId, { - // Long press snould only be called when there is valid control state, otherwise ignore - cvh.cws.control?.let { - cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - showDetail(cvh, it.getAppIntent()) - } - }, false /* blockable */)) + bouncerOrRun( + createAction( + cvh.cws.ci.controlId, + { + // Long press snould only be called when there is valid control state, + // otherwise ignore + cvh.cws.control?.let { + cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + showDetail(cvh, it.getAppIntent()) + } + }, + false /* blockable */ + ), + isAuthRequired(cvh) + ) } override fun runPendingAction(controlId: String) { @@ -135,6 +162,8 @@ class ControlActionCoordinatorImpl @Inject constructor( actionsInProgress.remove(controlId) } + private fun isAuthRequired(cvh: ControlViewHolder) = cvh.cws.control?.isAuthRequired() ?: true + private fun shouldRunAction(controlId: String) = if (actionsInProgress.add(controlId)) { uiExecutor.executeDelayed({ @@ -146,8 +175,8 @@ class ControlActionCoordinatorImpl @Inject constructor( } @VisibleForTesting - fun bouncerOrRun(action: Action) { - if (keyguardStateController.isShowing()) { + fun bouncerOrRun(action: Action, authRequired: Boolean) { + if (keyguardStateController.isShowing() && authRequired) { if (isLocked) { context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 17eb15b6a5565..55509d1ae0bdf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -94,6 +94,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { )) `when`(cvh.cws.ci.controlId).thenReturn(ID) + `when`(cvh.cws.control?.isAuthRequired()).thenReturn(true) action = spy(coordinator.Action(ID, {}, false)) doReturn(action).`when`(coordinator).createAction(any(), any(), anyBoolean()) } @@ -103,7 +104,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { `when`(keyguardStateController.isShowing()).thenReturn(false) coordinator.toggle(cvh, "", true) - verify(coordinator).bouncerOrRun(action) + verify(coordinator).bouncerOrRun(action, true /*authRequired */) verify(action).invoke() } @@ -113,7 +114,7 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { `when`(keyguardStateController.isUnlocked()).thenReturn(false) coordinator.toggle(cvh, "", true) - verify(coordinator).bouncerOrRun(action) + verify(coordinator).bouncerOrRun(action, true /*authRequired */) verify(activityStarter).dismissKeyguardThenExecute(any(), any(), anyBoolean()) verify(action, never()).invoke() @@ -127,4 +128,15 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { coordinator.runPendingAction(ID) verify(action).invoke() } + + @Test + fun testToggleRunsWhenLockedAndAuthNotRequired() { + `when`(keyguardStateController.isShowing()).thenReturn(true) + `when`(keyguardStateController.isUnlocked()).thenReturn(false) + `when`(cvh.cws.control?.isAuthRequired()).thenReturn(false) + + coordinator.toggle(cvh, "", true) + verify(coordinator).bouncerOrRun(action, false /* authRequired */) + verify(action).invoke() + } }