From 9430e71f88917ab90b4d8cef5bd88983f93ba6b3 Mon Sep 17 00:00:00 2001 From: Will Leshner Date: Fri, 27 Jan 2023 15:18:35 -0800 Subject: [PATCH] Implement assistant attention icon in dream overlay status bar. Bug: 265235507 Test: atest AssistantAttentionConditionTest Test: atest DreamOverlayStateControllerTest Test: atest DreamOverlayStatusBarViewControllerTest Change-Id: Ic184d89a5ca684539ffed7b0dc8582322dc51d2f --- ..._overlay_assistant_attention_indicator.xml | 32 +++++ .../layout/dream_overlay_status_bar_view.xml | 10 ++ packages/SystemUI/res/values/strings.xml | 2 + .../dagger/SystemUICoreStartableModule.kt | 7 ++ .../dreams/AssistantAttentionMonitor.java | 59 +++++++++ .../dreams/DreamOverlayStateController.java | 35 ++++++ .../dreams/DreamOverlayStatusBarView.java | 6 +- .../DreamOverlayStatusBarViewController.java | 18 ++- .../callbacks/AssistantAttentionCallback.java | 48 ++++++++ .../AssistantAttentionCondition.java | 95 +++++++++++++++ .../DreamOverlayStateControllerTest.java | 32 +++++ ...eamOverlayStatusBarViewControllerTest.java | 22 ++++ .../AssistantAttentionConditionTest.java | 114 ++++++++++++++++++ 13 files changed, 475 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/callbacks/AssistantAttentionCallback.java create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/conditions/AssistantAttentionCondition.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml new file mode 100644 index 0000000000000..dad2cdf8ede15 --- /dev/null +++ b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index 885e5e2d4441a..50dcaf333ea1f 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -111,5 +111,15 @@ android:visibility="gone" android:contentDescription="@string/dream_overlay_status_bar_camera_mic_off" /> + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 0f9462866cd49..71e325d7272a1 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2798,6 +2798,8 @@ Mic is off Camera and mic are off + + Assistant is listening {count, plural, =1 {# notification} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 48f90bc608b7a..caa0e96cf5fc6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -28,6 +28,7 @@ import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.UdfpsOverlay import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.dagger.qualifiers.PerUser +import com.android.systemui.dreams.AssistantAttentionMonitor import com.android.systemui.dreams.DreamMonitor import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.keyboard.KeyboardUI @@ -309,4 +310,10 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(DreamMonitor::class) abstract fun bindDreamMonitor(sysui: DreamMonitor): CoreStartable + + /**Inject into AssistantAttentionMonitor */ + @Binds + @IntoMap + @ClassKey(AssistantAttentionMonitor::class) + abstract fun bindAssistantAttentionMonitor(sysui: AssistantAttentionMonitor): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java b/packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java new file mode 100644 index 0000000000000..49d7f78107402 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/AssistantAttentionMonitor.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.dreams; + +import android.util.Log; + +import com.android.systemui.CoreStartable; +import com.android.systemui.dreams.callbacks.AssistantAttentionCallback; +import com.android.systemui.dreams.conditions.AssistantAttentionCondition; +import com.android.systemui.shared.condition.Monitor; + +import javax.inject.Inject; + +/** + * A {@link CoreStartable} to retain a monitor for tracking assistant attention. + */ +public class AssistantAttentionMonitor implements CoreStartable { + private static final String TAG = "AssistAttentionMonitor"; + + // We retain a reference to the monitor so it is not garbage-collected. + private final Monitor mConditionMonitor; + private final AssistantAttentionCondition mAssistantAttentionCondition; + private final AssistantAttentionCallback mCallback; + + @Inject + public AssistantAttentionMonitor( + Monitor monitor, + AssistantAttentionCondition assistantAttentionCondition, + AssistantAttentionCallback callback) { + mConditionMonitor = monitor; + mAssistantAttentionCondition = assistantAttentionCondition; + mCallback = callback; + + } + @Override + public void start() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "started"); + } + + mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(mCallback) + .addCondition(mAssistantAttentionCondition) + .build()); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java index 2c7ecb1182f27..b7f6a70ecad85 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java @@ -56,6 +56,8 @@ public class DreamOverlayStateController implements public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1; public static final int STATE_DREAM_ENTRY_ANIMATIONS_FINISHED = 1 << 2; public static final int STATE_DREAM_EXIT_ANIMATIONS_RUNNING = 1 << 3; + public static final int STATE_HAS_ASSISTANT_ATTENTION = 1 << 4; + public static final int STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE = 1 << 5; private static final int OP_CLEAR_STATE = 1; private static final int OP_SET_STATE = 2; @@ -251,6 +253,22 @@ public class DreamOverlayStateController implements return containsState(STATE_DREAM_EXIT_ANIMATIONS_RUNNING); } + /** + * Returns whether assistant currently has the user's attention. + * @return {@code true} if assistant has the user's attention, {@code false} otherwise. + */ + public boolean hasAssistantAttention() { + return containsState(STATE_HAS_ASSISTANT_ATTENTION); + } + + /** + * Returns whether the dream overlay status bar is currently visible. + * @return {@code true} if the status bar is visible, {@code false} otherwise. + */ + public boolean isDreamOverlayStatusBarVisible() { + return containsState(STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE); + } + private boolean containsState(int state) { return (mState & state) != 0; } @@ -309,6 +327,23 @@ public class DreamOverlayStateController implements STATE_DREAM_EXIT_ANIMATIONS_RUNNING); } + /** + * Sets whether assistant currently has the user's attention. + * @param hasAttention {@code true} if has the user's attention, {@code false} otherwise. + */ + public void setHasAssistantAttention(boolean hasAttention) { + modifyState(hasAttention ? OP_SET_STATE : OP_CLEAR_STATE, STATE_HAS_ASSISTANT_ATTENTION); + } + + /** + * Sets whether the dream overlay status bar is visible. + * @param visible {@code true} if the status bar is visible, {@code false} otherwise. + */ + public void setDreamOverlayStatusBarVisible(boolean visible) { + modifyState( + visible ? OP_SET_STATE : OP_CLEAR_STATE, STATE_DREAM_OVERLAY_STATUS_BAR_VISIBLE); + } + /** * Returns the available complication types. */ diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index 96bce4cd3cd9f..7c1bfeda30b28 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -52,7 +52,8 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { STATUS_ICON_CAMERA_DISABLED, STATUS_ICON_MIC_DISABLED, STATUS_ICON_MIC_CAMERA_DISABLED, - STATUS_ICON_PRIORITY_MODE_ON + STATUS_ICON_PRIORITY_MODE_ON, + STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE, }) public @interface StatusIconType {} public static final int STATUS_ICON_NOTIFICATIONS = 0; @@ -62,6 +63,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public static final int STATUS_ICON_MIC_DISABLED = 4; public static final int STATUS_ICON_MIC_CAMERA_DISABLED = 5; public static final int STATUS_ICON_PRIORITY_MODE_ON = 6; + public static final int STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE = 7; private final Map mStatusIcons = new HashMap<>(); private Context mContext; @@ -132,6 +134,8 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { fetchStatusIconForResId(R.id.dream_overlay_notification_indicator)); mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON, addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode))); + mStatusIcons.put(STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE, + fetchStatusIconForResId(R.id.dream_overlay_assistant_attention_indicator)); mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status); mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java index 90c440c403ec4..2221a04d3aa42 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java @@ -113,6 +113,7 @@ public class DreamOverlayStatusBarViewController extends ViewController provider.removeCallback(mNotificationCountCallback)); mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback); mView.removeAllExtraStatusBarItemViews(); + mDreamOverlayStateController.setDreamOverlayStatusBarVisible(false); mDreamOverlayStateController.removeCallback(mDreamOverlayStateCallback); mTouchInsetSession.clear(); @@ -270,12 +272,20 @@ public class DreamOverlayStatusBarViewController extends ViewController callbackCapture = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(callbackCapture.capture()); + callbackCapture.getValue().onStateChanged(); + + verify(mView).showIcon( + DreamOverlayStatusBarView.STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE, true, null); + } + @Test public void testStatusBarHiddenWhenSystemStatusBarShown() { mController.onViewAttached(); @@ -572,6 +587,13 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase { assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE); } + @Test + public void testDreamOverlayStatusBarVisibleSetToFalseOnDetach() { + mController.onViewAttached(); + mController.onViewDetached(); + verify(mDreamOverlayStateController).setDreamOverlayStatusBarVisible(false); + } + private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) { when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show); final ArgumentCaptor diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java new file mode 100644 index 0000000000000..ef1061faeed9d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2023 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 com.android.systemui.dreams.conditions; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.RemoteException; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.internal.app.AssistUtils; +import com.android.internal.app.IVisualQueryDetectionAttentionListener; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.shared.condition.Condition; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AssistantAttentionConditionTest extends SysuiTestCase { + @Mock + Condition.Callback mCallback; + @Mock + AssistUtils mAssistUtils; + @Mock + DreamOverlayStateController mDreamOverlayStateController; + + private AssistantAttentionCondition mAssistantAttentionCondition; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mAssistantAttentionCondition = + new AssistantAttentionCondition(mDreamOverlayStateController, mAssistUtils); + // Adding a callback also starts the condition. + mAssistantAttentionCondition.addCallback(mCallback); + } + + @Test + public void testEnableVisualQueryDetection() { + final ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture()); + + when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); + argumentCaptor.getValue().onStateChanged(); + + verify(mAssistUtils).enableVisualQueryDetection(any()); + } + + @Test + public void testDisableVisualQueryDetection() { + final ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(argumentCaptor.capture()); + + when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); + argumentCaptor.getValue().onStateChanged(); + when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(false); + argumentCaptor.getValue().onStateChanged(); + + verify(mAssistUtils).disableVisualQueryDetection(); + } + + @Test + public void testAttentionChangedTriggersCondition() throws RemoteException { + final ArgumentCaptor callbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(callbackCaptor.capture()); + + when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true); + callbackCaptor.getValue().onStateChanged(); + + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(IVisualQueryDetectionAttentionListener.class); + verify(mAssistUtils).enableVisualQueryDetection(listenerCaptor.capture()); + + listenerCaptor.getValue().onAttentionGained(); + assertThat(mAssistantAttentionCondition.isConditionMet()).isTrue(); + + listenerCaptor.getValue().onAttentionLost(); + assertThat(mAssistantAttentionCondition.isConditionMet()).isFalse(); + + verify(mCallback, times(2)).onConditionChanged(eq(mAssistantAttentionCondition)); + } +}