Merge "Controls API - Support action while device is locked"
This commit is contained in:
committed by
Android (Google) Code Review
commit
c8cd8cdd7b
@@ -38383,6 +38383,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<android.service.controls.Control> CREATOR;
|
||||
field public static final int STATUS_DISABLED = 4; // 0x4
|
||||
@@ -38397,6 +38398,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);
|
||||
|
||||
@@ -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<Control> CREATOR = new Creator<Control>() {
|
||||
@@ -409,6 +423,7 @@ public final class Control implements Parcelable {
|
||||
* <li> Status: {@link Status#STATUS_UNKNOWN}
|
||||
* <li> Control template: {@link ControlTemplate#getNoTemplateObject}
|
||||
* <li> Status text: {@code ""}
|
||||
* <li> Auth Required: {@code true}
|
||||
* </ul>
|
||||
*/
|
||||
@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 {
|
||||
* <li> Status: {@link Status#STATUS_UNKNOWN}
|
||||
* <li> Control template: {@link ControlTemplate#getNoTemplateObject}
|
||||
* <li> Status text: {@code ""}
|
||||
* <li> Auth Required: {@code true}
|
||||
* </ul>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Control> createPublisherFor(@NonNull List<String> 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<Integer> consumer);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user