Merge "Implement assistant attention icon in dream overlay status bar."

This commit is contained in:
William Leshner
2023-02-01 04:00:17 +00:00
committed by Android (Google) Code Review
13 changed files with 475 additions and 5 deletions

View File

@@ -0,0 +1,32 @@
<!--
~ 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.
-->
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="24dp"
android:viewportWidth="56"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M12 0H44C50.6274 0 56 5.37258 56 12C56 18.6274 50.6274 24 44 24H12C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0Z"
/>
<path
android:pathData="M0 0V24H56V0"
android:fillColor="#FFFFFF"
/>
</group>
</vector>

View File

@@ -111,5 +111,15 @@
android:visibility="gone"
android:contentDescription="@string/dream_overlay_status_bar_camera_mic_off" />
<ImageView
android:id="@+id/dream_overlay_assistant_attention_indicator"
android:layout_width="@dimen/dream_overlay_grey_chip_width"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
android:src="@drawable/dream_overlay_assistant_attention_indicator"
android:visibility="gone"
android:contentDescription=
"@string/dream_overlay_status_bar_assistant_attention_indicator" />
</LinearLayout>
</com.android.systemui.dreams.DreamOverlayStatusBarView>

View File

@@ -2798,6 +2798,8 @@
<string name="dream_overlay_status_bar_mic_off">Mic is off</string>
<!-- Content description for the camera and mic off icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
<string name="dream_overlay_status_bar_camera_mic_off">Camera and mic are off</string>
<!-- Content description for the assistant attention indicator [CHAR LIMIT=NONE] -->
<string name="dream_overlay_status_bar_assistant_attention_indicator">Assistant is listening</string>
<!-- Content description for the notifications indicator icon in the dream overlay status bar [CHAR LIMIT=NONE] -->
<string name="dream_overlay_status_bar_notification_indicator">{count, plural,
=1 {# notification}

View File

@@ -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
}

View File

@@ -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());
}
}

View File

@@ -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.
*/

View File

@@ -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<Integer, View> 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);

View File

@@ -113,6 +113,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
mEntryAnimationsFinished =
mDreamOverlayStateController.areEntryAnimationsFinished();
updateVisibility();
updateAssistantAttentionIcon();
}
};
@@ -214,6 +215,7 @@ public class DreamOverlayStatusBarViewController extends ViewController<DreamOve
provider -> 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<DreamOve
hasAlarm ? buildAlarmContentDescription(alarm) : null);
}
private void updateAssistantAttentionIcon() {
showIcon(DreamOverlayStatusBarView.STATUS_ICON_ASSISTANT_ATTENTION_ACTIVE,
mDreamOverlayStateController.hasAssistantAttention());
}
private void updateVisibility() {
if (shouldShowStatusBar()) {
mView.setVisibility(View.VISIBLE);
} else {
mView.setVisibility(View.INVISIBLE);
final int currentVisibility = mView.getVisibility();
final int newVisibility = shouldShowStatusBar() ? View.VISIBLE : View.INVISIBLE;
if (currentVisibility == newVisibility) {
return;
}
mView.setVisibility(newVisibility);
mDreamOverlayStateController.setDreamOverlayStatusBarVisible(newVisibility == View.VISIBLE);
}
private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) {

View File

@@ -0,0 +1,48 @@
/*
* 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.callbacks;
import android.util.Log;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.shared.condition.Monitor;
import javax.inject.Inject;
/**
* A callback that informs {@link DreamOverlayStateController} when assistant has the user's
* attention.
*/
public class AssistantAttentionCallback implements Monitor.Callback {
private static final String TAG = "AssistAttentionCallback";
private final DreamOverlayStateController mStateController;
@Inject
public AssistantAttentionCallback(DreamOverlayStateController stateController) {
mStateController = stateController;
}
@Override
public void onConditionsChanged(boolean allConditionsMet) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onConditionChanged:" + allConditionsMet);
}
mStateController.setHasAssistantAttention(allConditionsMet);
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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 com.android.internal.app.AssistUtils;
import com.android.internal.app.IVisualQueryDetectionAttentionListener;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.shared.condition.Condition;
import javax.inject.Inject;
/**
* {@link AssistantAttentionCondition} provides a signal when assistant has the user's attention.
*/
public class AssistantAttentionCondition extends Condition {
private final DreamOverlayStateController mDreamOverlayStateController;
private final AssistUtils mAssistUtils;
private boolean mEnabled;
private final IVisualQueryDetectionAttentionListener mVisualQueryDetectionAttentionListener =
new IVisualQueryDetectionAttentionListener.Stub() {
@Override
public void onAttentionGained() {
updateCondition(true);
}
@Override
public void onAttentionLost() {
updateCondition(false);
}
};
private final DreamOverlayStateController.Callback mCallback =
new DreamOverlayStateController.Callback() {
@Override
public void onStateChanged() {
if (mDreamOverlayStateController.isDreamOverlayStatusBarVisible()) {
enableVisualQueryDetection();
} else {
disableVisualQueryDetection();
}
}
};
@Inject
public AssistantAttentionCondition(
DreamOverlayStateController dreamOverlayStateController,
AssistUtils assistUtils) {
mDreamOverlayStateController = dreamOverlayStateController;
mAssistUtils = assistUtils;
}
@Override
protected void start() {
mDreamOverlayStateController.addCallback(mCallback);
}
@Override
protected void stop() {
disableVisualQueryDetection();
mDreamOverlayStateController.removeCallback(mCallback);
}
private void enableVisualQueryDetection() {
if (mEnabled) {
return;
}
mEnabled = true;
mAssistUtils.enableVisualQueryDetection(mVisualQueryDetectionAttentionListener);
}
private void disableVisualQueryDetection() {
if (!mEnabled) {
return;
}
mEnabled = false;
mAssistUtils.disableVisualQueryDetection();
// Make sure the condition is set to false as well.
updateCondition(false);
}
}

View File

@@ -289,4 +289,36 @@ public class DreamOverlayStateControllerTest extends SysuiTestCase {
verify(mCallback, times(1)).onStateChanged();
assertThat(stateController.areEntryAnimationsFinished()).isTrue();
}
@Test
public void testNotifyDreamOverlayStatusBarVisibleChanged() {
final DreamOverlayStateController stateController =
new DreamOverlayStateController(mExecutor, true);
stateController.addCallback(mCallback);
mExecutor.runAllReady();
assertThat(stateController.isDreamOverlayStatusBarVisible()).isFalse();
stateController.setDreamOverlayStatusBarVisible(true);
mExecutor.runAllReady();
verify(mCallback, times(1)).onStateChanged();
assertThat(stateController.isDreamOverlayStatusBarVisible()).isTrue();
}
@Test
public void testNotifyHasAssistantAttentionChanged() {
final DreamOverlayStateController stateController =
new DreamOverlayStateController(mExecutor, true);
stateController.addCallback(mCallback);
mExecutor.runAllReady();
assertThat(stateController.hasAssistantAttention()).isFalse();
stateController.setHasAssistantAttention(true);
mExecutor.runAllReady();
verify(mCallback, times(1)).onStateChanged();
assertThat(stateController.hasAssistantAttention()).isTrue();
}
}

View File

@@ -467,6 +467,21 @@ public class DreamOverlayStatusBarViewControllerTest extends SysuiTestCase {
DreamOverlayStatusBarView.STATUS_ICON_PRIORITY_MODE_ON, false, null);
}
@Test
public void testAssistantAttentionIconShownWhenAttentionGained() {
mController.onViewAttached();
when(mDreamOverlayStateController.hasAssistantAttention()).thenReturn(true);
final ArgumentCaptor<DreamOverlayStateController.Callback> 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<StatusBarWindowStateListener>

View File

@@ -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<DreamOverlayStateController.Callback> 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<DreamOverlayStateController.Callback> 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<DreamOverlayStateController.Callback> callbackCaptor =
ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
verify(mDreamOverlayStateController).addCallback(callbackCaptor.capture());
when(mDreamOverlayStateController.isDreamOverlayStatusBarVisible()).thenReturn(true);
callbackCaptor.getValue().onStateChanged();
final ArgumentCaptor<IVisualQueryDetectionAttentionListener> 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));
}
}