Merge "Move animation flows to immediate dispatcher" into main
diff --git a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java
index fbe67a4..c34936f 100644
--- a/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java
+++ b/apct-tests/perftests/core/src/android/text/VariableFontPerfTest.java
@@ -19,6 +19,7 @@
import android.graphics.Paint;
import android.graphics.RecordingCanvas;
import android.graphics.RenderNode;
+import android.graphics.Typeface;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
@@ -120,13 +121,34 @@
public void testSetFontVariationSettings() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
final Paint paint = new Paint(PAINT);
- final Random random = new Random(0);
while (state.keepRunning()) {
state.pauseTiming();
- int weight = random.nextInt(1000);
+ paint.setTypeface(null);
+ paint.setFontVariationSettings(null);
+ Typeface.clearTypefaceCachesForTestingPurpose();
state.resumeTiming();
- paint.setFontVariationSettings("'wght' " + weight);
+ paint.setFontVariationSettings("'wght' 450");
}
+ Typeface.clearTypefaceCachesForTestingPurpose();
}
+
+ @Test
+ public void testSetFontVariationSettings_Cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final Paint paint = new Paint(PAINT);
+ Typeface.clearTypefaceCachesForTestingPurpose();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ paint.setTypeface(null);
+ paint.setFontVariationSettings(null);
+ state.resumeTiming();
+
+ paint.setFontVariationSettings("'wght' 450");
+ }
+
+ Typeface.clearTypefaceCachesForTestingPurpose();
+ }
+
}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3d190fe..5c5a2f6 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -267,3 +267,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "typeface_cache_for_var_settings"
+ namespace: "text"
+ description: "Cache Typeface instance for font variation settings."
+ bug: "355462362"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
\ No newline at end of file
diff --git a/core/java/android/view/contentprotection/OWNERS b/core/java/android/view/contentprotection/OWNERS
index b3583a7..48052c6 100644
--- a/core/java/android/view/contentprotection/OWNERS
+++ b/core/java/android/view/contentprotection/OWNERS
@@ -1,4 +1,6 @@
-# Bug component: 544200
+# Bug component: 1040349
-include /core/java/android/view/contentcapture/OWNERS
+njagar@google.com
+williamluh@google.com
+aaronjosephs@google.com
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
index 572a599..fcc3023 100644
--- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java
@@ -48,6 +48,7 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
@@ -423,6 +424,14 @@
for (IProtoLogGroup group : protoLogGroups) {
mLogGroups.put(group.name(), group);
}
+
+ final var hasGroupsLoggingToLogcat = Arrays.stream(protoLogGroups)
+ .anyMatch(IProtoLogGroup::isLogToLogcat);
+
+ final ILogger logger = (msg) -> Slog.i(TAG, msg);
+ if (hasGroupsLoggingToLogcat) {
+ mViewerConfig.loadViewerConfig(logger, mLegacyViewerConfigFilename);
+ }
}
}
diff --git a/core/res/res/drawable-car/car_activity_resolver_list_background.xml b/core/res/res/drawable-car/car_activity_resolver_list_background.xml
deleted file mode 100644
index dbbadd8..0000000
--- a/core/res/res/drawable-car/car_activity_resolver_list_background.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Copyright (C) 2024 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <solid android:color="?attr/colorBackgroundFloating" />
- <corners android:radius="@dimen/car_activity_resolver_corner_radius" />
-</shape>
\ No newline at end of file
diff --git a/core/res/res/layout-car/car_resolver_list.xml b/core/res/res/layout-car/car_resolver_list.xml
deleted file mode 100644
index 08c9861..0000000
--- a/core/res/res/layout-car/car_resolver_list.xml
+++ /dev/null
@@ -1,127 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright 2019, 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.
-*/
--->
-<com.android.internal.widget.ResolverDrawerLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/car_activity_resolver_width"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:id="@id/contentPanel">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:background="@drawable/car_activity_resolver_list_background">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@drawable/car_activity_resolver_list_background"
- android:orientation="horizontal"
- android:paddingVertical="@dimen/car_padding_4"
- android:paddingHorizontal="@dimen/car_padding_4" >
- <TextView
- android:id="@+id/profile_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="gone" />
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="start"
- android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
- </LinearLayout>
-
- <FrameLayout
- android:id="@+id/stub"
- android:visibility="gone"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
-
- <TabHost
- android:id="@+id/profile_tabhost"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_centerHorizontal="true"
- android:background="?android:attr/colorBackgroundFloating">
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <TabWidget
- android:id="@android:id/tabs"
- android:visibility="gone"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- </TabWidget>
- <View
- android:id="@+id/resolver_tab_divider"
- android:visibility="gone"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
- <FrameLayout
- android:id="@android:id/tabcontent"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <com.android.internal.app.ResolverViewPager
- android:id="@+id/profile_pager"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- </FrameLayout>
- </LinearLayout>
- </TabHost>
-
- <LinearLayout
- android:id="@+id/button_bar"
- android:visibility="gone"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginVertical="@dimen/car_padding_4"
- android:layout_marginHorizontal="@dimen/car_padding_4"
- android:padding="0dp"
- android:gravity="center"
- android:background="@drawable/car_activity_resolver_list_background"
- android:orientation="vertical">
-
- <Button
- android:id="@+id/button_once"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_button_height"
- android:enabled="false"
- android:layout_gravity="center"
- android:layout_marginBottom="@dimen/car_padding_2"
- android:text="@string/activity_resolver_use_once"
- android:onClick="onButtonClick"/>
-
- <Button
- android:id="@+id/button_always"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_button_height"
- android:enabled="false"
- android:layout_gravity="center"
- android:text="@string/activity_resolver_use_always"
- android:onClick="onButtonClick"/>
- </LinearLayout>
-
- </LinearLayout>
-
-</com.android.internal.widget.ResolverDrawerLayout>
\ No newline at end of file
diff --git a/core/res/res/layout-car/car_resolver_list_with_default.xml b/core/res/res/layout-car/car_resolver_list_with_default.xml
deleted file mode 100644
index 08cc7ff..0000000
--- a/core/res/res/layout-car/car_resolver_list_with_default.xml
+++ /dev/null
@@ -1,159 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-* Copyright 2019, 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.
-*/
--->
-<com.android.internal.widget.ResolverDrawerLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/car_activity_resolver_width"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:id="@id/contentPanel">
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:layout_gravity="center"
- android:background="@drawable/car_activity_resolver_list_background">
-
- <FrameLayout
- android:id="@+id/stub"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@drawable/car_activity_resolver_list_background"/>
-
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="@dimen/car_activity_resolver_list_item_height"
- android:orientation="horizontal">
-
- <RadioButton
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:focusable="false"
- android:clickable="false"
- android:layout_marginStart="?attr/listPreferredItemPaddingStart"
- android:layout_gravity="start|center_vertical"
- android:checked="true"/>
-
- <ImageView
- android:id="@+id/icon"
- android:layout_width="@dimen/car_icon_size"
- android:layout_height="@dimen/car_icon_size"
- android:layout_gravity="start|center_vertical"
- android:layout_marginStart="@dimen/car_padding_4"
- android:src="@drawable/resolver_icon_placeholder"
- android:scaleType="fitCenter"/>
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginHorizontal="?attr/listPreferredItemPaddingStart"
- style="?android:attr/textAppearanceListItem"
- android:layout_gravity="start|center_vertical" />
-
- <LinearLayout
- android:id="@+id/profile_button"
- android:visibility="gone"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
-
- <ImageView
- android:id="@+id/icon"
- android:visibility="gone"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <TextView
- android:id="@id/text1"
- android:visibility="gone"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- </LinearLayout>
- </LinearLayout>
-
- <TabHost
- android:id="@+id/profile_tabhost"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_centerHorizontal="true"
- android:background="?attr/colorBackgroundFloating">
- <LinearLayout
- android:orientation="vertical"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <TabWidget
- android:id="@android:id/tabs"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:visibility="gone">
- </TabWidget>
- <View
- android:id="@+id/resolver_tab_divider"
- android:visibility="gone"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
- <FrameLayout
- android:id="@android:id/tabcontent"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <com.android.internal.app.ResolverViewPager
- android:id="@+id/profile_pager"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
- </com.android.internal.app.ResolverViewPager>
- </FrameLayout>
- </LinearLayout>
- </TabHost>
-
- <LinearLayout
- android:id="@+id/button_bar"
- android:visibility="gone"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginVertical="@dimen/car_padding_4"
- android:layout_marginHorizontal="@dimen/car_padding_4"
- android:gravity="center"
- android:background="@drawable/car_activity_resolver_list_background"
- android:orientation="vertical">
-
- <Button
- android:id="@+id/button_once"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_button_height"
- android:enabled="false"
- android:layout_gravity="center"
- android:layout_marginBottom="@dimen/car_padding_2"
- android:text="@string/activity_resolver_use_once"
- android:onClick="onButtonClick"/>
-
- <Button
- android:id="@+id/button_always"
- android:layout_width="match_parent"
- android:layout_height="@dimen/car_button_height"
- android:enabled="false"
- android:layout_gravity="center"
- android:text="@string/activity_resolver_use_always"
- android:onClick="onButtonClick"/>
- </LinearLayout>
- </LinearLayout>
-
-</com.android.internal.widget.ResolverDrawerLayout>
diff --git a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java
new file mode 100644
index 0000000..8a54e5b
--- /dev/null
+++ b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015 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 android.graphics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.test.InstrumentationTestCase;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * PaintTest tests {@link Paint}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PaintFontVariationTest extends InstrumentationTestCase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+ @Test
+ public void testDerivedFromSameTypeface() {
+ final Paint p = new Paint();
+
+ p.setTypeface(Typeface.SANS_SERIF);
+ assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+ Typeface first = p.getTypeface();
+
+ p.setTypeface(Typeface.SANS_SERIF);
+ assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+ Typeface second = p.getTypeface();
+
+ assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+ @Test
+ public void testDerivedFromChained() {
+ final Paint p = new Paint();
+
+ p.setTypeface(Typeface.SANS_SERIF);
+ assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+ Typeface first = p.getTypeface();
+
+ assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+ Typeface second = p.getTypeface();
+
+ assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+ }
+}
diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java
index 0dec756..878ba70 100644
--- a/core/tests/coretests/src/android/graphics/PaintTest.java
+++ b/core/tests/coretests/src/android/graphics/PaintTest.java
@@ -16,13 +16,22 @@
package android.graphics;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertNotEquals;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.test.InstrumentationTestCase;
import android.text.TextUtils;
import androidx.test.filters.SmallTest;
+import com.android.text.flags.Flags;
+
+import org.junit.Rule;
+
import java.util.Arrays;
import java.util.HashSet;
@@ -30,6 +39,9 @@
* PaintTest tests {@link Paint}.
*/
public class PaintTest extends InstrumentationTestCase {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf";
static void assertEquals(String message, float[] expected, float[] actual) {
@@ -403,4 +415,33 @@
assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
}
+
+ @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+ public void testDerivedFromSameTypeface() {
+ final Paint p = new Paint();
+
+ p.setTypeface(Typeface.SANS_SERIF);
+ assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+ Typeface first = p.getTypeface();
+
+ p.setTypeface(Typeface.SANS_SERIF);
+ assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+ Typeface second = p.getTypeface();
+
+ assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
+ public void testDerivedFromChained() {
+ final Paint p = new Paint();
+
+ p.setTypeface(Typeface.SANS_SERIF);
+ assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
+ Typeface first = p.getTypeface();
+
+ assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
+ Typeface second = p.getTypeface();
+
+ assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
+ }
}
diff --git a/core/tests/coretests/src/android/view/contentprotection/OWNERS b/core/tests/coretests/src/android/view/contentprotection/OWNERS
index b3583a7..3d09da3 100644
--- a/core/tests/coretests/src/android/view/contentprotection/OWNERS
+++ b/core/tests/coretests/src/android/view/contentprotection/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 544200
+# Bug component: 1040349
-include /core/java/android/view/contentcapture/OWNERS
+include /core/java/android/view/contentprotection/OWNERS
diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java
index fd78816..889a7785 100644
--- a/graphics/java/android/graphics/Typeface.java
+++ b/graphics/java/android/graphics/Typeface.java
@@ -56,6 +56,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
+import com.android.text.flags.Flags;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -74,6 +75,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -143,6 +145,23 @@
private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16);
private static final Object sDynamicCacheLock = new Object();
+ private static final LruCache<Long, LruCache<String, Typeface>> sVariableCache =
+ new LruCache<>(16);
+ private static final Object sVariableCacheLock = new Object();
+
+ /** @hide */
+ @VisibleForTesting
+ public static void clearTypefaceCachesForTestingPurpose() {
+ synchronized (sWeightCacheLock) {
+ sWeightTypefaceCache.clear();
+ }
+ synchronized (sDynamicCacheLock) {
+ sDynamicTypefaceCache.evictAll();
+ }
+ synchronized (sVariableCacheLock) {
+ sVariableCache.evictAll();
+ }
+ }
@GuardedBy("SYSTEM_FONT_MAP_LOCK")
static Typeface sDefaultTypeface;
@@ -195,6 +214,8 @@
@UnsupportedAppUsage
public final long native_instance;
+ private final Typeface mDerivedFrom;
+
private final String mSystemFontFamilyName;
private final Runnable mCleaner;
@@ -274,6 +295,18 @@
}
/**
+ * Returns the Typeface used for creating this Typeface.
+ *
+ * Maybe null if this is not derived from other Typeface.
+ * TODO(b/357707916): Make this public API.
+ * @hide
+ */
+ @VisibleForTesting
+ public final @Nullable Typeface getDerivedFrom() {
+ return mDerivedFrom;
+ }
+
+ /**
* Returns the system font family name if the typeface was created from a system font family,
* otherwise returns null.
*/
@@ -1021,9 +1054,51 @@
return typeface;
}
- /** @hide */
+ private static String axesToVarKey(@NonNull List<FontVariationAxis> axes) {
+ // The given list can be mutated because it is allocated in Paint#setFontVariationSettings.
+ // Currently, Paint#setFontVariationSettings is the only code path reaches this method.
+ axes.sort(Comparator.comparingInt(FontVariationAxis::getOpenTypeTagValue));
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < axes.size(); ++i) {
+ final FontVariationAxis fva = axes.get(i);
+ sb.append(fva.getTag());
+ sb.append(fva.getStyleValue());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * TODO(b/357707916): Make this public API.
+ * @hide
+ */
public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family,
@NonNull List<FontVariationAxis> axes) {
+ if (Flags.typefaceCacheForVarSettings()) {
+ final Typeface target = (family == null) ? Typeface.DEFAULT : family;
+ final Typeface base = (target.mDerivedFrom == null) ? target : target.mDerivedFrom;
+
+ final String key = axesToVarKey(axes);
+
+ synchronized (sVariableCacheLock) {
+ LruCache<String, Typeface> innerCache = sVariableCache.get(base.native_instance);
+ if (innerCache == null) {
+ // Cache up to 16 var instance per root Typeface
+ innerCache = new LruCache<>(16);
+ sVariableCache.put(base.native_instance, innerCache);
+ } else {
+ Typeface cached = innerCache.get(key);
+ if (cached != null) {
+ return cached;
+ }
+ }
+ Typeface typeface = new Typeface(
+ nativeCreateFromTypefaceWithVariation(base.native_instance, axes),
+ base.getSystemFontFamilyName(), base);
+ innerCache.put(key, typeface);
+ return typeface;
+ }
+ }
+
final Typeface base = family == null ? Typeface.DEFAULT : family;
Typeface typeface = new Typeface(
nativeCreateFromTypefaceWithVariation(base.native_instance, axes),
@@ -1184,11 +1259,19 @@
// don't allow clients to call this directly
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private Typeface(long ni) {
- this(ni, null);
+ this(ni, null, null);
+ }
+
+
+ // don't allow clients to call this directly
+ // This is kept for robolectric.
+ private Typeface(long ni, @Nullable String systemFontFamilyName) {
+ this(ni, systemFontFamilyName, null);
}
// don't allow clients to call this directly
- private Typeface(long ni, @Nullable String systemFontFamilyName) {
+ private Typeface(long ni, @Nullable String systemFontFamilyName,
+ @Nullable Typeface derivedFrom) {
if (ni == 0) {
throw new RuntimeException("native typeface cannot be made");
}
@@ -1198,6 +1281,7 @@
mStyle = nativeGetStyle(ni);
mWeight = nativeGetWeight(ni);
mSystemFontFamilyName = systemFontFamilyName;
+ mDerivedFrom = derivedFrom;
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 456d92a..544c2dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -1108,16 +1108,21 @@
/** Handle task closing by removing wallpaper activity if it's the last active task */
private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
logV("handleTaskClosing")
+ if (!isDesktopModeShowing(task.displayId))
+ return null
+
val wct = WindowContainerTransaction()
if (taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
- && taskRepository.wallpaperActivityToken != null) {
+ && taskRepository.wallpaperActivityToken != null
+ ) {
// Remove wallpaper activity when the last active task is removed
removeWallpaperActivity(wct)
}
taskRepository.addClosingTask(task.displayId, task.taskId)
// If a CLOSE or TO_BACK is triggered on a desktop task, remove the task.
if (DesktopModeFlags.BACK_NAVIGATION.isEnabled(context) &&
- taskRepository.isVisibleTask(task.taskId)) {
+ taskRepository.isVisibleTask(task.taskId)
+ ) {
wct.removeTask(task.token)
}
return if (wct.isEmpty) null else wct
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 18c6228..a630cef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1819,6 +1819,19 @@
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ )
+ fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() {
+ val task = setUpFreeformTask()
+ markTaskHidden(task)
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 66b9e5c..5632e30 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -991,6 +991,16 @@
}
flag {
+ name: "communal_widget_trampoline_fix"
+ namespace: "systemui"
+ description: "fixes activity starts caused by non-activity trampolines from widgets."
+ bug: "350468769"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "app_clips_backlinks"
namespace: "systemui"
description: "Enables Backlinks improvement feature in App Clips"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index f655ac1..d164eab 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -95,7 +95,7 @@
import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
@@ -114,7 +114,7 @@
@Composable
fun BouncerContent(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
dialogFactory: BouncerDialogFactory,
modifier: Modifier = Modifier,
) {
@@ -128,7 +128,7 @@
@VisibleForTesting
fun BouncerContent(
layout: BouncerSceneLayout,
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
dialogFactory: BouncerDialogFactory,
modifier: Modifier
) {
@@ -173,7 +173,7 @@
*/
@Composable
private fun StandardLayout(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
val isHeightExpanded =
@@ -235,7 +235,7 @@
*/
@Composable
private fun SplitLayout(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
val authMethod by viewModel.authMethodViewModel.collectAsStateWithLifecycle()
@@ -326,7 +326,7 @@
*/
@Composable
private fun BesideUserSwitcherLayout(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
val layoutDirection = LocalLayoutDirection.current
@@ -461,7 +461,7 @@
/** Arranges the bouncer contents and user switcher contents one on top of the other, vertically. */
@Composable
private fun BelowUserSwitcherLayout(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
Column(
@@ -506,7 +506,7 @@
@Composable
private fun FoldAware(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
aboveFold: @Composable BoxScope.() -> Unit,
belowFold: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
@@ -649,7 +649,7 @@
*/
@Composable
private fun OutputArea(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
val authMethodViewModel: AuthMethodBouncerViewModel? by
@@ -677,7 +677,7 @@
*/
@Composable
private fun InputArea(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
pinButtonRowVerticalSpacing: Dp,
centerPatternDotsVertically: Boolean,
modifier: Modifier = Modifier,
@@ -706,7 +706,7 @@
@Composable
private fun ActionArea(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
val actionButton: BouncerActionButtonModel? by
@@ -774,7 +774,7 @@
@Composable
private fun Dialog(
- bouncerViewModel: BouncerViewModel,
+ bouncerViewModel: BouncerSceneContentViewModel,
dialogFactory: BouncerDialogFactory,
) {
val dialogViewModel by bouncerViewModel.dialogViewModel.collectAsStateWithLifecycle()
@@ -803,7 +803,7 @@
/** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
@Composable
private fun UserSwitcher(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
modifier: Modifier = Modifier,
) {
if (!viewModel.isUserSwitcherVisible) {
@@ -884,7 +884,7 @@
@Composable
private fun UserSwitcherDropdownMenu(
isExpanded: Boolean,
- items: List<BouncerViewModel.UserSwitcherDropdownItemViewModel>,
+ items: List<BouncerSceneContentViewModel.UserSwitcherDropdownItemViewModel>,
onDismissed: () -> Unit,
) {
val context = LocalContext.current
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 9fd30b4..3a46882 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -27,9 +27,11 @@
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.bouncer.ui.BouncerDialogFactory
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
import javax.inject.Inject
@@ -51,23 +53,37 @@
class BouncerScene
@Inject
constructor(
- private val viewModel: BouncerViewModel,
+ private val actionsViewModelFactory: BouncerSceneActionsViewModel.Factory,
+ private val contentViewModelFactory: BouncerSceneContentViewModel.Factory,
private val dialogFactory: BouncerDialogFactory,
) : ComposableScene {
override val key = Scenes.Bouncer
+ private val actionsViewModel: BouncerSceneActionsViewModel by lazy {
+ actionsViewModelFactory.create()
+ }
+
override val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- viewModel.destinationScenes
+ actionsViewModel.actions
+
+ override suspend fun activate() {
+ actionsViewModel.activate()
+ }
@Composable
override fun SceneScope.Content(
modifier: Modifier,
- ) = BouncerScene(viewModel, dialogFactory, modifier)
+ ) =
+ BouncerScene(
+ viewModel = rememberViewModel { contentViewModelFactory.create() },
+ dialogFactory = dialogFactory,
+ modifier = modifier,
+ )
}
@Composable
private fun SceneScope.BouncerScene(
- viewModel: BouncerViewModel,
+ viewModel: BouncerSceneContentViewModel,
dialogFactory: BouncerDialogFactory,
modifier: Modifier = Modifier,
) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index c9fa671..deef652 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -22,14 +22,14 @@
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -39,17 +39,16 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
- private val underTest by lazy {
- PinBouncerViewModel(
- applicationContext = context,
- viewModelScope = testScope.backgroundScope,
- interactor = bouncerInteractor,
+ private val underTest =
+ kosmos.pinBouncerViewModelFactory.create(
isInputEnabled = MutableStateFlow(true),
- simBouncerInteractor = kosmos.simBouncerInteractor,
- authenticationMethod = AuthenticationMethodModel.Pin,
onIntentionalUserInput = {},
+ authenticationMethod = AuthenticationMethodModel.Pin,
)
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
index 4f5d0e5..b83ab7e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -52,6 +52,7 @@
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
@@ -87,6 +88,7 @@
intArrayOf(ignoreHelpMessageId)
)
underTest = kosmos.bouncerMessageViewModel
+ underTest.activateIn(testScope)
overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
kosmos.fakeSystemPropertiesHelper.set(
DeviceUnlockedInteractor.SYS_BOOT_REASON_PROP,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt
new file mode 100644
index 0000000..a86a0c0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModelTest.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.testKosmos
+import com.android.systemui.truth.containsEntriesExactly
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class BouncerSceneActionsViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private lateinit var underTest: BouncerSceneActionsViewModel
+
+ @Before
+ fun setUp() {
+ kosmos.sceneContainerStartable.start()
+ underTest = kosmos.bouncerSceneActionsViewModel
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ fun actions() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings)
+ runCurrent()
+
+ kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer)
+ runCurrent()
+
+ assertThat(actions)
+ .containsEntriesExactly(
+ Back to UserActionResult(Scenes.QuickSettings),
+ Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings),
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
similarity index 88%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
index ccddc9c..9bddcd2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModelTest.kt
@@ -18,10 +18,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
-import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
@@ -38,11 +34,9 @@
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.domain.startable.sceneContainerStartable
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.testKosmos
-import com.android.systemui.truth.containsEntriesExactly
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -62,17 +56,18 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
-class BouncerViewModelTest : SysuiTestCase() {
+class BouncerSceneContentViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private lateinit var underTest: BouncerViewModel
+ private lateinit var underTest: BouncerSceneContentViewModel
@Before
fun setUp() {
kosmos.sceneContainerStartable.start()
- underTest = kosmos.bouncerViewModel
+ underTest = kosmos.bouncerSceneContentViewModel
+ underTest.activateIn(testScope)
}
@Test
@@ -201,23 +196,6 @@
assertThat(isFoldSplitRequired).isTrue()
}
- @Test
- fun destinationScenes() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings)
- runCurrent()
-
- kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer)
- runCurrent()
-
- assertThat(destinationScenes)
- .containsEntriesExactly(
- Back to UserActionResult(Scenes.QuickSettings),
- Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings),
- )
- }
-
private fun authMethodsToTest(): List<AuthenticationMethodModel> {
return listOf(None, Pin, Password, Pattern, Sim)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index a09189e..492543f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -31,6 +31,7 @@
import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository
import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -44,7 +45,6 @@
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
@@ -68,12 +68,8 @@
private val isInputEnabled = MutableStateFlow(true)
private val underTest =
- PasswordBouncerViewModel(
- viewModelScope = testScope.backgroundScope,
- isInputEnabled = isInputEnabled.asStateFlow(),
- interactor = bouncerInteractor,
- inputMethodInteractor = inputMethodInteractor,
- selectedUserInteractor = selectedUserInteractor,
+ kosmos.passwordBouncerViewModelFactory.create(
+ isInputEnabled = isInputEnabled,
onIntentionalUserInput = {},
)
@@ -81,6 +77,7 @@
fun setUp() {
overrideResource(R.string.keyguard_enter_your_password, ENTER_YOUR_PASSWORD)
overrideResource(R.string.kg_wrong_password, WRONG_PASSWORD)
+ underTest.activateIn(testScope)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 14d3634..7c773a9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -26,9 +26,9 @@
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -54,17 +54,12 @@
private val testScope = kosmos.testScope
private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
private val sceneInteractor by lazy { kosmos.sceneInteractor }
- private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
- private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
- private val underTest by lazy {
- PatternBouncerViewModel(
- applicationContext = context,
- viewModelScope = testScope.backgroundScope,
- interactor = bouncerInteractor,
+ private val bouncerViewModel by lazy { kosmos.bouncerSceneContentViewModel }
+ private val underTest =
+ kosmos.patternBouncerViewModelFactory.create(
isInputEnabled = MutableStateFlow(true).asStateFlow(),
onIntentionalUserInput = {},
)
- }
private val containerSize = 90 // px
private val dotSize = 30 // px
@@ -73,6 +68,7 @@
fun setUp() {
overrideResource(R.string.keyguard_enter_your_pattern, ENTER_YOUR_PATTERN)
overrideResource(R.string.kg_wrong_pattern, WRONG_PATTERN)
+ underTest.activateIn(testScope)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 89bafb9..8d82e97 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -31,10 +31,9 @@
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -44,7 +43,6 @@
import kotlin.random.nextInt
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -62,24 +60,18 @@
private val testScope = kosmos.testScope
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
- private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
- private lateinit var underTest: PinBouncerViewModel
+ private val underTest =
+ kosmos.pinBouncerViewModelFactory.create(
+ isInputEnabled = MutableStateFlow(true),
+ onIntentionalUserInput = {},
+ authenticationMethod = AuthenticationMethodModel.Pin,
+ )
@Before
fun setUp() {
- underTest =
- PinBouncerViewModel(
- applicationContext = context,
- viewModelScope = testScope.backgroundScope,
- interactor = bouncerInteractor,
- isInputEnabled = MutableStateFlow(true).asStateFlow(),
- simBouncerInteractor = kosmos.simBouncerInteractor,
- authenticationMethod = AuthenticationMethodModel.Pin,
- onIntentionalUserInput = {},
- )
-
overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
overrideResource(R.string.kg_wrong_pin, WRONG_PIN)
+ underTest.activateIn(testScope)
}
@Test
@@ -96,14 +88,10 @@
fun simBouncerViewModel_simAreaIsVisible() =
testScope.runTest {
val underTest =
- PinBouncerViewModel(
- applicationContext = context,
- viewModelScope = testScope.backgroundScope,
- interactor = bouncerInteractor,
- isInputEnabled = MutableStateFlow(true).asStateFlow(),
- simBouncerInteractor = kosmos.simBouncerInteractor,
- authenticationMethod = AuthenticationMethodModel.Sim,
+ kosmos.pinBouncerViewModelFactory.create(
+ isInputEnabled = MutableStateFlow(true),
onIntentionalUserInput = {},
+ authenticationMethod = AuthenticationMethodModel.Sim,
)
assertThat(underTest.isSimAreaVisible).isTrue()
@@ -125,14 +113,10 @@
fun simBouncerViewModel_autoConfirmEnabled_hintedPinLengthIsNull() =
testScope.runTest {
val underTest =
- PinBouncerViewModel(
- applicationContext = context,
- viewModelScope = testScope.backgroundScope,
- interactor = bouncerInteractor,
- isInputEnabled = MutableStateFlow(true).asStateFlow(),
- simBouncerInteractor = kosmos.simBouncerInteractor,
- authenticationMethod = AuthenticationMethodModel.Sim,
+ kosmos.pinBouncerViewModelFactory.create(
+ isInputEnabled = MutableStateFlow(true),
onIntentionalUserInput = {},
+ authenticationMethod = AuthenticationMethodModel.Pin,
)
kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -355,6 +339,7 @@
AuthenticationMethodModel.Pin
)
kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
+ runCurrent()
underTest.onPinButtonClicked(1)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt
new file mode 100644
index 0000000..b3ffc71
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorTest.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 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.communal.domain.interactor
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.usage.UsageEvents
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.usagestats.data.repository.fakeUsageStatsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WidgetTrampolineInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val activityStarter = kosmos.activityStarter
+ private val usageStatsRepository = kosmos.fakeUsageStatsRepository
+ private val taskStackChangeListeners = kosmos.taskStackChangeListeners
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val userTracker = kosmos.fakeUserTracker
+ private val systemClock = kosmos.fakeSystemClock
+
+ private val underTest = kosmos.widgetTrampolineInteractor
+
+ @Before
+ fun setUp() {
+ userTracker.set(listOf(MAIN_USER), 0)
+ systemClock.setCurrentTimeMillis(testScope.currentTime)
+ }
+
+ @Test
+ fun testNewTaskStartsWhileOnHub_triggersUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ moveTaskToFront()
+
+ verify(activityStarter).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testNewTaskStartsAfterExitingHub_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ transition(from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN)
+ moveTaskToFront()
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testNewTaskStartsAfterTimeout_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ advanceTime(2.seconds)
+ moveTaskToFront()
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testActivityResumedWhileOnHub_triggersUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testActivityResumedAfterExitingHub_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ transition(from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN)
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testActivityDestroyed_doesNotTriggerUnlock() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ addActivityEvent(UsageEvents.Event.ACTIVITY_DESTROYED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter, never()).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ @Test
+ fun testMultipleActivityEvents_triggersUnlockOnlyOnce() =
+ testScope.runTest {
+ transition(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GLANCEABLE_HUB)
+ backgroundScope.launch { underTest.waitForActivityStartAndDismissKeyguard() }
+ runCurrent()
+
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(10.milliseconds)
+ addActivityEvent(UsageEvents.Event.ACTIVITY_RESUMED)
+ advanceTime(1.seconds)
+
+ verify(activityStarter, times(1)).dismissKeyguardThenExecute(any(), anyOrNull(), any())
+ }
+
+ private fun TestScope.advanceTime(duration: Duration) {
+ systemClock.advanceTime(duration.inWholeMilliseconds)
+ advanceTimeBy(duration)
+ }
+
+ private fun TestScope.addActivityEvent(type: Int) {
+ usageStatsRepository.addEvent(
+ instanceId = 1,
+ user = MAIN_USER.userHandle,
+ packageName = "pkg.test",
+ timestamp = systemClock.currentTimeMillis(),
+ type = type,
+ )
+ runCurrent()
+ }
+
+ private fun TestScope.moveTaskToFront() {
+ taskStackChangeListeners.listenerImpl.onTaskMovedToFront(mock<RunningTaskInfo>())
+ runCurrent()
+ }
+
+ private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = from,
+ to = to,
+ value = 0.1f,
+ transitionState = TransitionState.STARTED,
+ ownerName = "test",
+ ),
+ TransitionStep(
+ from = from,
+ to = to,
+ value = 1f,
+ transitionState = TransitionState.FINISHED,
+ ownerName = "test",
+ ),
+ ),
+ testScope
+ )
+ runCurrent()
+ }
+
+ private companion object {
+ val MAIN_USER: UserInfo = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
index 023de52..400f736 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt
@@ -27,7 +27,9 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.widgetTrampolineInteractor
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.ActivityStarter
@@ -67,9 +69,11 @@
with(kosmos) {
underTest =
WidgetInteractionHandler(
+ applicationScope = applicationCoroutineScope,
activityStarter = activityStarter,
communalSceneInteractor = communalSceneInteractor,
logBuffer = logcatLogBuffer(),
+ widgetTrampolineInteractor = widgetTrampolineInteractor,
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 66e45ab..cd84abc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -36,10 +36,10 @@
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
-import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.coroutines.collectLastValue
@@ -139,7 +139,7 @@
private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor
- private lateinit var bouncerViewModel: BouncerViewModel
+ private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
private val lockscreenSceneActionsViewModel by lazy {
LockscreenSceneActionsViewModel(
@@ -187,7 +187,7 @@
}
bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor
- bouncerViewModel = kosmos.bouncerViewModel
+ bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
shadeSceneContentViewModel = kosmos.shadeSceneContentViewModel
shadeSceneActionsViewModel = kosmos.shadeSceneActionsViewModel
@@ -198,6 +198,7 @@
lockscreenSceneActionsViewModel.activateIn(testScope)
shadeSceneContentViewModel.activateIn(testScope)
shadeSceneActionsViewModel.activateIn(testScope)
+ bouncerSceneContentViewModel.activateIn(testScope)
assertWithMessage("Initial scene key mismatch!")
.that(sceneContainerViewModel.currentScene.value)
@@ -397,7 +398,7 @@
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
emulateUserDrivenTransition(to = upDestinationSceneKey)
- val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton)
+ val bouncerActionButton by collectLastValue(bouncerSceneContentViewModel.actionButton)
assertWithMessage("Bouncer action button not visible")
.that(bouncerActionButton)
.isNotNull()
@@ -417,7 +418,7 @@
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
emulateUserDrivenTransition(to = upDestinationSceneKey)
- val bouncerActionButton by collectLastValue(bouncerViewModel.actionButton)
+ val bouncerActionButton by collectLastValue(bouncerSceneContentViewModel.actionButton)
assertWithMessage("Bouncer action button not visible during call")
.that(bouncerActionButton)
.isNotNull()
@@ -568,7 +569,7 @@
bouncerSceneJob =
if (to == Scenes.Bouncer) {
testScope.backgroundScope.launch {
- bouncerViewModel.authMethodViewModel.collect {
+ bouncerSceneContentViewModel.authMethodViewModel.collect {
// Do nothing. Need this to turn this otherwise cold flow, hot.
}
}
@@ -644,7 +645,8 @@
assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
.that(getCurrentSceneInUi())
.isEqualTo(Scenes.Bouncer)
- val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
+ val authMethodViewModel by
+ collectLastValue(bouncerSceneContentViewModel.authMethodViewModel)
assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
.that(authMethodViewModel)
.isInstanceOf(PinBouncerViewModel::class.java)
@@ -672,7 +674,8 @@
assertWithMessage("Cannot enter PIN when not on the Bouncer scene!")
.that(getCurrentSceneInUi())
.isEqualTo(Scenes.Bouncer)
- val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel)
+ val authMethodViewModel by
+ collectLastValue(bouncerSceneContentViewModel.authMethodViewModel)
assertWithMessage("Cannot enter PIN when not using a PIN authentication method!")
.that(authMethodViewModel)
.isInstanceOf(PinBouncerViewModel::class.java)
@@ -719,7 +722,7 @@
/** Emulates the dismissal of the IME (soft keyboard). */
private fun TestScope.dismissIme() {
- (bouncerViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let {
+ (bouncerSceneContentViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let {
it.onImeDismissed()
runCurrent()
}
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 63ad41a..13cd2c5 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -53,6 +53,7 @@
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.shared.animation.PhysicsAnimator;
import com.android.wm.shell.shared.animation.PhysicsAnimator.SpringConfig;
@@ -63,13 +64,6 @@
public class SwipeHelper implements Gefingerpoken, Dumpable {
static final String TAG = "com.android.systemui.SwipeHelper";
private static final boolean DEBUG_INVALIDATE = false;
- private static final boolean CONSTRAIN_SWIPE = true;
- private static final boolean FADE_OUT_DURING_SWIPE = true;
- private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
-
- public static final int X = 0;
- public static final int Y = 1;
-
private static final float SWIPE_ESCAPE_VELOCITY = 500f; // dp/sec
private static final int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
private static final int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
@@ -171,10 +165,6 @@
mPagingTouchSlop = pagingTouchSlop;
}
- public void setDisableHardwareLayers(boolean disableHwLayers) {
- mDisableHwLayers = disableHwLayers;
- }
-
private float getPos(MotionEvent ev) {
return ev.getX();
}
@@ -253,13 +243,14 @@
float translation) {
float swipeProgress = getSwipeProgressForOffset(animView, translation);
if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
- if (FADE_OUT_DURING_SWIPE && dismissable) {
- if (!mDisableHwLayers) {
- if (swipeProgress != 0f && swipeProgress != 1f) {
- animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- } else {
- animView.setLayerType(View.LAYER_TYPE_NONE, null);
- }
+ if (dismissable
+ || (NotificationContentAlphaOptimization.isEnabled() && translation == 0)) {
+ // We need to reset the content alpha even when the view is not dismissible (eg.
+ // when Guts is visible)
+ if (swipeProgress != 0f && swipeProgress != 1f) {
+ animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ } else {
+ animView.setLayerType(View.LAYER_TYPE_NONE, null);
}
updateSwipeProgressAlpha(animView, getSwipeAlpha(swipeProgress));
}
@@ -436,9 +427,7 @@
duration = fixedDuration;
}
- if (!mDisableHwLayers) {
- animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- }
+ animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
@@ -493,9 +482,7 @@
if (endAction != null) {
endAction.accept(mCancelled);
}
- if (!mDisableHwLayers) {
- animView.setLayerType(View.LAYER_TYPE_NONE, null);
- }
+ animView.setLayerType(View.LAYER_TYPE_NONE, null);
onDismissChildWithAnimationFinished();
}
});
@@ -612,7 +599,11 @@
* view is being animated to dismiss or snap.
*/
public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) {
- updateSwipeProgressFromOffset(animView, canBeDismissed, value);
+ updateSwipeProgressFromOffset(
+ animView,
+ /* dismissable= */ canBeDismissed,
+ /* translation= */ value
+ );
}
private void snapChildInstantly(final View view) {
@@ -689,7 +680,7 @@
} else {
// don't let items that can't be dismissed be dragged more than
// maxScrollDistance
- if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection(
+ if (!mCallback.canChildBeDismissedInDirection(
mTouchedView,
delta > 0)) {
float size = getSize(mTouchedView);
@@ -761,8 +752,7 @@
protected boolean swipedFarEnough() {
float translation = getTranslation(mTouchedView);
- return DISMISS_IF_SWIPED_FAR_ENOUGH
- && Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize(
+ return Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize(
mTouchedView);
}
@@ -822,9 +812,18 @@
}
public void forceResetSwipeState(@NonNull View view) {
- if (view.getTranslationX() == 0) return;
+ if (view.getTranslationX() == 0
+ && (!NotificationContentAlphaOptimization.isEnabled() || view.getAlpha() == 1f)
+ ) {
+ // Don't do anything when translation is 0 and alpha is 1
+ return;
+ }
setTranslation(view, 0);
- updateSwipeProgressFromOffset(view, /* dismissable= */ true, 0);
+ updateSwipeProgressFromOffset(
+ view,
+ /* dismissable= */ true,
+ /* translation= */ 0
+ );
}
/** This method resets the swipe state, and if `resetAll` is true, also resets the snap state */
@@ -893,7 +892,6 @@
pw.append("mTranslation=").println(mTranslation);
pw.append("mCanCurrViewBeDimissed=").println(mCanCurrViewBeDimissed);
pw.append("mMenuRowIntercepting=").println(mMenuRowIntercepting);
- pw.append("mDisableHwLayers=").println(mDisableHwLayers);
pw.append("mDismissPendingMap: ").println(mDismissPendingMap.size());
if (!mDismissPendingMap.isEmpty()) {
mDismissPendingMap.forEach((view, animator) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index aebc50f..3410782 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -18,8 +18,6 @@
import android.app.AlertDialog
import android.content.Context
-import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.SystemUIDialog
@@ -27,13 +25,7 @@
import dagger.Module
import dagger.Provides
-@Module(
- includes =
- [
- BouncerViewModelModule::class,
- BouncerMessageViewModelModule::class,
- ],
-)
+@Module
interface BouncerViewModule {
/** Binds BouncerView to BouncerViewImpl and makes it injectable. */
@Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
index 78811a9..ad93a25 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
@@ -9,7 +9,7 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
import com.android.systemui.bouncer.ui.BouncerDialogFactory
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
@@ -40,7 +40,7 @@
@Inject
constructor(
val legacyInteractor: PrimaryBouncerInteractor,
- val viewModel: BouncerViewModel,
+ val viewModelFactory: BouncerSceneContentViewModel.Factory,
val dialogFactory: BouncerDialogFactory,
val authenticationInteractor: AuthenticationInteractor,
val viewMediatorCallback: ViewMediatorCallback?,
@@ -65,7 +65,7 @@
ComposeBouncerViewBinder.bind(
view,
deps.legacyInteractor,
- deps.viewModel,
+ deps.viewModelFactory,
deps.dialogFactory,
deps.authenticationInteractor,
deps.selectedUserInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
index eaca276..c1f7d59 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/ComposeBouncerViewBinder.kt
@@ -14,7 +14,8 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.composable.BouncerContent
-import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import kotlinx.coroutines.flow.collectLatest
@@ -25,7 +26,7 @@
fun bind(
view: ViewGroup,
legacyInteractor: PrimaryBouncerInteractor,
- viewModel: BouncerViewModel,
+ viewModelFactory: BouncerSceneContentViewModel.Factory,
dialogFactory: BouncerDialogFactory,
authenticationInteractor: AuthenticationInteractor,
selectedUserInteractor: SelectedUserInteractor,
@@ -48,7 +49,14 @@
this@repeatWhenAttached.lifecycle
}
)
- setContent { PlatformTheme { BouncerContent(viewModel, dialogFactory) } }
+ setContent {
+ PlatformTheme {
+ BouncerContent(
+ rememberViewModel { viewModelFactory.create() },
+ dialogFactory,
+ )
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 4fbf735..e7dd974 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -17,17 +17,18 @@
package com.android.systemui.bouncer.ui.viewmodel
import android.annotation.StringRes
+import com.android.app.tracing.coroutines.flow.collectLatest
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import kotlinx.coroutines.CoroutineScope
+import com.android.systemui.lifecycle.SysUiViewModel
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.receiveAsFlow
sealed class AuthMethodBouncerViewModel(
- protected val viewModelScope: CoroutineScope,
protected val interactor: BouncerInteractor,
/**
@@ -37,7 +38,7 @@
* being able to attempt to unlock the device.
*/
val isInputEnabled: StateFlow<Boolean>,
-) {
+) : SysUiViewModel() {
private val _animateFailure = MutableStateFlow(false)
/**
@@ -57,6 +58,29 @@
*/
@get:StringRes abstract val lockoutMessageId: Int
+ private val authenticationRequests = Channel<AuthenticationRequest>(Channel.BUFFERED)
+
+ override suspend fun onActivated() {
+ authenticationRequests.receiveAsFlow().collectLatest { request ->
+ if (!isInputEnabled.value) {
+ return@collectLatest
+ }
+
+ val authenticationResult =
+ interactor.authenticate(
+ input = request.input,
+ tryAutoConfirm = request.useAutoConfirm,
+ )
+
+ if (authenticationResult == AuthenticationResult.SKIPPED && request.useAutoConfirm) {
+ return@collectLatest
+ }
+
+ _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
+ clearInput()
+ }
+ }
+
/**
* Notifies that the UI has been hidden from the user (after any transitions have completed).
*/
@@ -92,14 +116,11 @@
input: List<Any> = getInput(),
useAutoConfirm: Boolean = false,
) {
- viewModelScope.launch {
- val authenticationResult = interactor.authenticate(input, useAutoConfirm)
- if (authenticationResult == AuthenticationResult.SKIPPED && useAutoConfirm) {
- return@launch
- }
- _animateFailure.value = authenticationResult != AuthenticationResult.SUCCEEDED
-
- clearInput()
- }
+ authenticationRequests.trySend(AuthenticationRequest(input, useAutoConfirm))
}
+
+ private data class AuthenticationRequest(
+ val input: List<Any>,
+ val useAutoConfirm: Boolean,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
index 31479f1..c3215b4 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -27,7 +27,6 @@
import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
import com.android.systemui.bouncer.shared.model.primaryMessage
import com.android.systemui.bouncer.shared.model.secondaryMessage
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryBiometricsAllowedInteractor
@@ -39,19 +38,19 @@
import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.time.SystemClock
-import dagger.Module
-import dagger.Provides
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlin.math.ceil
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -65,20 +64,21 @@
/** Holds UI state for the 2-line status message shown on the bouncer. */
@OptIn(ExperimentalCoroutinesApi::class)
-class BouncerMessageViewModel(
+class BouncerMessageViewModel
+@AssistedInject
+constructor(
@Application private val applicationContext: Context,
- @Application private val applicationScope: CoroutineScope,
private val bouncerInteractor: BouncerInteractor,
private val simBouncerInteractor: SimBouncerInteractor,
private val authenticationInteractor: AuthenticationInteractor,
- selectedUser: Flow<UserViewModel>,
+ private val userSwitcherViewModel: UserSwitcherViewModel,
private val clock: SystemClock,
private val biometricMessageInteractor: BiometricMessageInteractor,
private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
private val deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor,
- flags: ComposeBouncerFlags,
-) {
+ private val flags: ComposeBouncerFlags,
+) : SysUiViewModel() {
/**
* A message shown when the user has attempted the wrong credential too many times and now must
* wait a while before attempting to authenticate again.
@@ -94,6 +94,25 @@
/** The user-facing message to show in the bouncer. */
val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+ override suspend fun onActivated() {
+ if (!flags.isComposeBouncerOrSceneContainerEnabled()) {
+ return
+ }
+
+ coroutineScope {
+ launch {
+ // Update the lockout countdown whenever the selected user is switched.
+ userSwitcherViewModel.selectedUser.collect { startLockoutCountdown() }
+ }
+
+ launch { defaultBouncerMessageInitializer() }
+ launch { listenForSimBouncerEvents() }
+ launch { listenForBouncerEvents() }
+ launch { listenForFaceMessages() }
+ launch { listenForFingerprintMessages() }
+ }
+ }
+
/** Initializes the bouncer message to default whenever it is shown. */
fun onShown() {
showDefaultMessage()
@@ -108,173 +127,161 @@
private var lockoutCountdownJob: Job? = null
- private fun defaultBouncerMessageInitializer() {
- applicationScope.launch {
- resetToDefault.emit(Unit)
- authenticationInteractor.authenticationMethod
- .flatMapLatest { authMethod ->
- if (authMethod == AuthenticationMethodModel.Sim) {
- resetToDefault.map {
- MessageViewModel(simBouncerInteractor.getDefaultMessage())
- }
- } else if (authMethod.isSecure) {
- combine(
- deviceUnlockedInteractor.deviceEntryRestrictionReason,
- lockoutMessage,
- deviceEntryBiometricsAllowedInteractor
- .isFingerprintCurrentlyAllowedOnBouncer,
- resetToDefault,
- ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
- lockoutMsg
- ?: deviceEntryRestrictedReason.toMessage(
- authMethod,
- isFpAllowedInBouncer
- )
- }
- } else {
- emptyFlow()
+ private suspend fun defaultBouncerMessageInitializer() {
+ resetToDefault.emit(Unit)
+ authenticationInteractor.authenticationMethod
+ .flatMapLatest { authMethod ->
+ if (authMethod == AuthenticationMethodModel.Sim) {
+ resetToDefault.map {
+ MessageViewModel(simBouncerInteractor.getDefaultMessage())
}
+ } else if (authMethod.isSecure) {
+ combine(
+ deviceUnlockedInteractor.deviceEntryRestrictionReason,
+ lockoutMessage,
+ deviceEntryBiometricsAllowedInteractor
+ .isFingerprintCurrentlyAllowedOnBouncer,
+ resetToDefault,
+ ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
+ lockoutMsg
+ ?: deviceEntryRestrictedReason.toMessage(
+ authMethod,
+ isFpAllowedInBouncer
+ )
+ }
+ } else {
+ emptyFlow()
}
- .collectLatest { messageViewModel -> message.value = messageViewModel }
- }
+ }
+ .collectLatest { messageViewModel -> message.value = messageViewModel }
}
- private fun listenForSimBouncerEvents() {
+ private suspend fun listenForSimBouncerEvents() {
// Listen for any events from the SIM bouncer and update the message shown on the bouncer.
- applicationScope.launch {
- authenticationInteractor.authenticationMethod
- .flatMapLatest { authMethod ->
- if (authMethod == AuthenticationMethodModel.Sim) {
- simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
- simMsg?.let { MessageViewModel(it) }
- }
- } else {
- emptyFlow()
+ authenticationInteractor.authenticationMethod
+ .flatMapLatest { authMethod ->
+ if (authMethod == AuthenticationMethodModel.Sim) {
+ simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
+ simMsg?.let { MessageViewModel(it) }
}
+ } else {
+ emptyFlow()
}
- .collectLatest {
- if (it != null) {
- message.value = it
- } else {
- resetToDefault.emit(Unit)
- }
+ }
+ .collectLatest {
+ if (it != null) {
+ message.value = it
+ } else {
+ resetToDefault.emit(Unit)
}
- }
+ }
}
- private fun listenForFaceMessages() {
+ private suspend fun listenForFaceMessages() {
// Listen for any events from face authentication and update the message shown on the
// bouncer.
- applicationScope.launch {
- biometricMessageInteractor.faceMessage
- .sample(
- authenticationInteractor.authenticationMethod,
- deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer,
- )
- .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
- val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
- val defaultPrimaryMessage =
- BouncerMessageStrings.defaultMessage(
- authMethod,
- fingerprintAllowedOnBouncer
+ biometricMessageInteractor.faceMessage
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+ )
+ .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
+ val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
+ val defaultPrimaryMessage =
+ BouncerMessageStrings.defaultMessage(authMethod, fingerprintAllowedOnBouncer)
+ .primaryMessage
+ .toResString()
+ message.value =
+ when (faceMessage) {
+ is FaceTimeoutMessage ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = faceMessage.message,
+ isUpdateAnimated = true
)
- .primaryMessage
- .toResString()
- message.value =
- when (faceMessage) {
- is FaceTimeoutMessage ->
- MessageViewModel(
- text = defaultPrimaryMessage,
- secondaryText = faceMessage.message,
- isUpdateAnimated = true
- )
- is FaceLockoutMessage ->
- if (isFaceAuthStrong)
- BouncerMessageStrings.class3AuthLockedOut(authMethod)
- .toMessage()
- else
- BouncerMessageStrings.faceLockedOut(
- authMethod,
- fingerprintAllowedOnBouncer
- )
- .toMessage()
- is FaceFailureMessage ->
- BouncerMessageStrings.incorrectFaceInput(
+ is FaceLockoutMessage ->
+ if (isFaceAuthStrong)
+ BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+ else
+ BouncerMessageStrings.faceLockedOut(
authMethod,
fingerprintAllowedOnBouncer
)
.toMessage()
- else ->
- MessageViewModel(
- text = defaultPrimaryMessage,
- secondaryText = faceMessage.message,
- isUpdateAnimated = false
+ is FaceFailureMessage ->
+ BouncerMessageStrings.incorrectFaceInput(
+ authMethod,
+ fingerprintAllowedOnBouncer
)
- }
- delay(MESSAGE_DURATION)
- resetToDefault.emit(Unit)
- }
- }
- }
-
- private fun listenForFingerprintMessages() {
- applicationScope.launch {
- // Listen for any events from fingerprint authentication and update the message shown
- // on the bouncer.
- biometricMessageInteractor.fingerprintMessage
- .sample(
- authenticationInteractor.authenticationMethod,
- deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer
- )
- .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
- val defaultPrimaryMessage =
- BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
- .primaryMessage
- .toResString()
- message.value =
- when (fingerprintMessage) {
- is FingerprintLockoutMessage ->
- BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
- is FingerprintFailureMessage ->
- BouncerMessageStrings.incorrectFingerprintInput(authMethod)
- .toMessage()
- else ->
- MessageViewModel(
- text = defaultPrimaryMessage,
- secondaryText = fingerprintMessage.message,
- isUpdateAnimated = false
- )
- }
- delay(MESSAGE_DURATION)
- resetToDefault.emit(Unit)
- }
- }
- }
-
- private fun listenForBouncerEvents() {
- // Keeps the lockout message up-to-date.
- applicationScope.launch {
- bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() }
- }
-
- // Listens to relevant bouncer events
- applicationScope.launch {
- bouncerInteractor.onIncorrectBouncerInput
- .sample(
- authenticationInteractor.authenticationMethod,
- deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer
- )
- .collectLatest { (_, authMethod, isFingerprintAllowed) ->
- message.emit(
- BouncerMessageStrings.incorrectSecurityInput(
- authMethod,
- isFingerprintAllowed
+ .toMessage()
+ else ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = faceMessage.message,
+ isUpdateAnimated = false
)
- .toMessage()
+ }
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+
+ private suspend fun listenForFingerprintMessages() {
+ // Listen for any events from fingerprint authentication and update the message shown
+ // on the bouncer.
+ biometricMessageInteractor.fingerprintMessage
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ deviceEntryBiometricsAllowedInteractor.isFingerprintCurrentlyAllowedOnBouncer
+ )
+ .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
+ val defaultPrimaryMessage =
+ BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
+ .primaryMessage
+ .toResString()
+ message.value =
+ when (fingerprintMessage) {
+ is FingerprintLockoutMessage ->
+ BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+ is FingerprintFailureMessage ->
+ BouncerMessageStrings.incorrectFingerprintInput(authMethod).toMessage()
+ else ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = fingerprintMessage.message,
+ isUpdateAnimated = false
+ )
+ }
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+
+ private suspend fun listenForBouncerEvents() {
+ coroutineScope {
+ // Keeps the lockout message up-to-date.
+ launch { bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } }
+
+ // Listens to relevant bouncer events
+ launch {
+ bouncerInteractor.onIncorrectBouncerInput
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ deviceEntryBiometricsAllowedInteractor
+ .isFingerprintCurrentlyAllowedOnBouncer
)
- delay(MESSAGE_DURATION)
- resetToDefault.emit(Unit)
- }
+ .collectLatest { (_, authMethod, isFingerprintAllowed) ->
+ message.emit(
+ BouncerMessageStrings.incorrectSecurityInput(
+ authMethod,
+ isFingerprintAllowed
+ )
+ .toMessage()
+ )
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
}
}
@@ -323,10 +330,10 @@
}
/** Shows the countdown message and refreshes it every second. */
- private fun startLockoutCountdown() {
+ private suspend fun startLockoutCountdown() {
lockoutCountdownJob?.cancel()
- lockoutCountdownJob =
- applicationScope.launch {
+ lockoutCountdownJob = coroutineScope {
+ launch {
authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
do {
val remainingSeconds = remainingLockoutSeconds()
@@ -352,6 +359,7 @@
lockoutCountdownJob = null
}
}
+ }
}
private fun remainingLockoutSeconds(): Int {
@@ -365,20 +373,9 @@
private fun Int.toResString(): String = applicationContext.getString(this)
- init {
- if (flags.isComposeBouncerOrSceneContainerEnabled()) {
- applicationScope.launch {
- // Update the lockout countdown whenever the selected user is switched.
- selectedUser.collect { startLockoutCountdown() }
- }
-
- defaultBouncerMessageInitializer()
-
- listenForSimBouncerEvents()
- listenForBouncerEvents()
- listenForFaceMessages()
- listenForFingerprintMessages()
- }
+ @AssistedFactory
+ interface Factory {
+ fun create(): BouncerMessageViewModel
}
companion object {
@@ -398,40 +395,3 @@
*/
val isUpdateAnimated: Boolean = true,
)
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@Module
-object BouncerMessageViewModelModule {
-
- @Provides
- @SysUISingleton
- fun viewModel(
- @Application applicationContext: Context,
- @Application applicationScope: CoroutineScope,
- bouncerInteractor: BouncerInteractor,
- simBouncerInteractor: SimBouncerInteractor,
- authenticationInteractor: AuthenticationInteractor,
- clock: SystemClock,
- biometricMessageInteractor: BiometricMessageInteractor,
- faceAuthInteractor: DeviceEntryFaceAuthInteractor,
- deviceUnlockedInteractor: DeviceUnlockedInteractor,
- deviceEntryBiometricsAllowedInteractor: DeviceEntryBiometricsAllowedInteractor,
- flags: ComposeBouncerFlags,
- userSwitcherViewModel: UserSwitcherViewModel,
- ): BouncerMessageViewModel {
- return BouncerMessageViewModel(
- applicationContext = applicationContext,
- applicationScope = applicationScope,
- bouncerInteractor = bouncerInteractor,
- simBouncerInteractor = simBouncerInteractor,
- authenticationInteractor = authenticationInteractor,
- clock = clock,
- biometricMessageInteractor = biometricMessageInteractor,
- faceAuthInteractor = faceAuthInteractor,
- deviceUnlockedInteractor = deviceUnlockedInteractor,
- deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor,
- flags = flags,
- selectedUser = userSwitcherViewModel.selectedUser,
- )
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt
new file mode 100644
index 0000000..2a27271
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneActionsViewModel.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 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.bouncer.ui.viewmodel
+
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
+
+/**
+ * Models UI state for user actions that can lead to navigation to other scenes when showing the
+ * bouncer scene.
+ */
+class BouncerSceneActionsViewModel
+@AssistedInject
+constructor(
+ private val bouncerInteractor: BouncerInteractor,
+) : SceneActionsViewModel() {
+
+ override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+ bouncerInteractor.dismissDestination
+ .map { prevScene ->
+ mapOf(
+ Back to UserActionResult(prevScene),
+ Swipe(SwipeDirection.Down) to UserActionResult(prevScene),
+ )
+ }
+ .collectLatest { actions -> setActions(actions) }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): BouncerSceneActionsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
new file mode 100644
index 0000000..aede63b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerSceneContentViewModel.kt
@@ -0,0 +1,362 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.type
+import androidx.core.graphics.drawable.toBitmap
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
+import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.SysUiViewModel
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Models UI state for the content of the bouncer scene. */
+class BouncerSceneContentViewModel
+@AssistedInject
+constructor(
+ @Application private val applicationContext: Context,
+ private val bouncerInteractor: BouncerInteractor,
+ private val authenticationInteractor: AuthenticationInteractor,
+ private val devicePolicyManager: DevicePolicyManager,
+ private val bouncerMessageViewModelFactory: BouncerMessageViewModel.Factory,
+ private val flags: ComposeBouncerFlags,
+ private val userSwitcher: UserSwitcherViewModel,
+ private val actionButtonInteractor: BouncerActionButtonInteractor,
+ private val pinViewModelFactory: PinBouncerViewModel.Factory,
+ private val patternViewModelFactory: PatternBouncerViewModel.Factory,
+ private val passwordViewModelFactory: PasswordBouncerViewModel.Factory,
+) : SysUiViewModel() {
+ private val _selectedUserImage = MutableStateFlow<Bitmap?>(null)
+ val selectedUserImage: StateFlow<Bitmap?> = _selectedUserImage.asStateFlow()
+
+ val message: BouncerMessageViewModel by lazy { bouncerMessageViewModelFactory.create() }
+
+ private val _userSwitcherDropdown =
+ MutableStateFlow<List<UserSwitcherDropdownItemViewModel>>(emptyList())
+ val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
+ _userSwitcherDropdown.asStateFlow()
+
+ val isUserSwitcherVisible: Boolean
+ get() = bouncerInteractor.isUserSwitcherVisible
+
+ /** View-model for the current UI, based on the current authentication method. */
+ private val _authMethodViewModel = MutableStateFlow<AuthMethodBouncerViewModel?>(null)
+ val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> =
+ _authMethodViewModel.asStateFlow()
+
+ /**
+ * A message for a dialog to show when the user has attempted the wrong credential too many
+ * times and now must wait a while before attempting again.
+ *
+ * If `null`, the lockout dialog should not be shown.
+ */
+ private val lockoutDialogMessage = MutableStateFlow<String?>(null)
+
+ /**
+ * A message for a dialog to show when the user has attempted the wrong credential too many
+ * times and their user/profile/device data is at risk of being wiped due to a Device Manager
+ * policy.
+ *
+ * If `null`, the wipe dialog should not be shown.
+ */
+ private val wipeDialogMessage = MutableStateFlow<String?>(null)
+
+ private val _dialogViewModel = MutableStateFlow<DialogViewModel?>(createDialogViewModel())
+ /**
+ * Models the dialog to be shown to the user, or `null` if no dialog should be shown.
+ *
+ * Once the dialog is shown, the UI should call [DialogViewModel.onDismiss] when the user
+ * dismisses this dialog.
+ */
+ val dialogViewModel: StateFlow<DialogViewModel?> = _dialogViewModel.asStateFlow()
+
+ private val _actionButton = MutableStateFlow<BouncerActionButtonModel?>(null)
+ /**
+ * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
+ * be shown.
+ */
+ val actionButton: StateFlow<BouncerActionButtonModel?> = _actionButton.asStateFlow()
+
+ private val _isSideBySideSupported =
+ MutableStateFlow(isSideBySideSupported(authMethodViewModel.value))
+ /**
+ * Whether the "side-by-side" layout is supported.
+ *
+ * When presented on its own, without a user switcher (e.g. not on communal devices like
+ * tablets, for example), some authentication method UIs don't do well if they're shown in the
+ * side-by-side layout; these need to be shown with the standard layout so they can take up as
+ * much width as possible.
+ */
+ val isSideBySideSupported: StateFlow<Boolean> = _isSideBySideSupported.asStateFlow()
+
+ private val _isFoldSplitRequired =
+ MutableStateFlow(isFoldSplitRequired(authMethodViewModel.value))
+ /**
+ * Whether the splitting the UI around the fold seam (where the hinge is on a foldable device)
+ * is required.
+ */
+ val isFoldSplitRequired: StateFlow<Boolean> = _isFoldSplitRequired.asStateFlow()
+
+ private val _isInputEnabled =
+ MutableStateFlow(authenticationInteractor.lockoutEndTimestamp == null)
+ private val isInputEnabled: StateFlow<Boolean> = _isInputEnabled.asStateFlow()
+
+ override suspend fun onActivated() {
+ coroutineScope {
+ launch { message.activate() }
+ launch {
+ authenticationInteractor.authenticationMethod
+ .map(::getChildViewModel)
+ .collectLatest { childViewModelOrNull ->
+ _authMethodViewModel.value = childViewModelOrNull
+ childViewModelOrNull?.activate()
+ }
+ }
+
+ launch {
+ authenticationInteractor.upcomingWipe.collect { wipeModel ->
+ wipeDialogMessage.value = wipeModel?.message
+ }
+ }
+
+ launch {
+ userSwitcher.selectedUser
+ .map { it.image.toBitmap() }
+ .collectLatest { _selectedUserImage.value = it }
+ }
+
+ launch {
+ combine(
+ userSwitcher.users,
+ userSwitcher.menu,
+ ) { users, actions ->
+ users.map { user ->
+ UserSwitcherDropdownItemViewModel(
+ icon = Icon.Loaded(user.image, contentDescription = null),
+ text = user.name,
+ onClick = user.onClicked ?: {},
+ )
+ } +
+ actions.map { action ->
+ UserSwitcherDropdownItemViewModel(
+ icon =
+ Icon.Resource(
+ action.iconResourceId,
+ contentDescription = null
+ ),
+ text = Text.Resource(action.textResourceId),
+ onClick = action.onClicked,
+ )
+ }
+ }
+ .collectLatest { _userSwitcherDropdown.value = it }
+ }
+
+ launch {
+ combine(wipeDialogMessage, lockoutDialogMessage) { _, _ -> createDialogViewModel() }
+ .collectLatest { _dialogViewModel.value = it }
+ }
+
+ launch {
+ actionButtonInteractor.actionButton.collectLatest { _actionButton.value = it }
+ }
+
+ launch {
+ authMethodViewModel
+ .map { authMethod -> isSideBySideSupported(authMethod) }
+ .collectLatest { _isSideBySideSupported.value = it }
+ }
+
+ launch {
+ authMethodViewModel
+ .map { authMethod -> isFoldSplitRequired(authMethod) }
+ .collectLatest { _isFoldSplitRequired.value = it }
+ }
+
+ launch {
+ message.isLockoutMessagePresent
+ .map { lockoutMessagePresent -> !lockoutMessagePresent }
+ .collectLatest { _isInputEnabled.value = it }
+ }
+ }
+ }
+
+ private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
+ return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
+ }
+
+ private fun isFoldSplitRequired(authMethod: AuthMethodBouncerViewModel?): Boolean {
+ return authMethod !is PasswordBouncerViewModel
+ }
+
+ private fun getChildViewModel(
+ authenticationMethod: AuthenticationMethodModel,
+ ): AuthMethodBouncerViewModel? {
+ // If the current child view-model matches the authentication method, reuse it instead of
+ // creating a new instance.
+ val childViewModel = authMethodViewModel.value
+ if (authenticationMethod == childViewModel?.authenticationMethod) {
+ return childViewModel
+ }
+
+ return when (authenticationMethod) {
+ is AuthenticationMethodModel.Pin ->
+ pinViewModelFactory.create(
+ authenticationMethod = authenticationMethod,
+ onIntentionalUserInput = ::onIntentionalUserInput,
+ isInputEnabled = isInputEnabled,
+ )
+ is AuthenticationMethodModel.Sim ->
+ pinViewModelFactory.create(
+ authenticationMethod = authenticationMethod,
+ onIntentionalUserInput = ::onIntentionalUserInput,
+ isInputEnabled = isInputEnabled,
+ )
+ is AuthenticationMethodModel.Password ->
+ passwordViewModelFactory.create(
+ onIntentionalUserInput = ::onIntentionalUserInput,
+ isInputEnabled = isInputEnabled,
+ )
+ is AuthenticationMethodModel.Pattern ->
+ patternViewModelFactory.create(
+ onIntentionalUserInput = ::onIntentionalUserInput,
+ isInputEnabled = isInputEnabled,
+ )
+ else -> null
+ }
+ }
+
+ private fun onIntentionalUserInput() {
+ message.showDefaultMessage()
+ bouncerInteractor.onIntentionalUserInput()
+ }
+
+ /**
+ * @return A message warning the user that the user/profile/device will be wiped upon a further
+ * [AuthenticationWipeModel.remainingAttempts] unsuccessful authentication attempts.
+ */
+ private fun AuthenticationWipeModel.getAlmostAtWipeMessage(): String {
+ val message =
+ applicationContext.getString(
+ wipeTarget.messageIdForAlmostWipe,
+ failedAttempts,
+ remainingAttempts,
+ )
+ return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
+ devicePolicyManager.resources.getString(
+ DevicePolicyResources.Strings.SystemUi
+ .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
+ { message },
+ failedAttempts,
+ remainingAttempts,
+ ) ?: message
+ } else {
+ message
+ }
+ }
+
+ /**
+ * @return A message informing the user that their user/profile/device will be wiped promptly.
+ */
+ private fun AuthenticationWipeModel.getWipeMessage(): String {
+ val message = applicationContext.getString(wipeTarget.messageIdForWipe, failedAttempts)
+ return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
+ devicePolicyManager.resources.getString(
+ DevicePolicyResources.Strings.SystemUi
+ .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
+ { message },
+ failedAttempts,
+ ) ?: message
+ } else {
+ message
+ }
+ }
+
+ private val AuthenticationWipeModel.message: String
+ get() = if (remainingAttempts > 0) getAlmostAtWipeMessage() else getWipeMessage()
+
+ private fun createDialogViewModel(): DialogViewModel? {
+ val wipeText = wipeDialogMessage.value
+ val lockoutText = lockoutDialogMessage.value
+ return when {
+ // The wipe dialog takes priority over the lockout dialog.
+ wipeText != null ->
+ DialogViewModel(
+ text = wipeText,
+ onDismiss = { wipeDialogMessage.value = null },
+ )
+ lockoutText != null ->
+ DialogViewModel(
+ text = lockoutText,
+ onDismiss = { lockoutDialogMessage.value = null },
+ )
+ else -> null // No dialog to show.
+ }
+ }
+
+ /**
+ * Notifies that a key event has occurred.
+ *
+ * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
+ */
+ fun onKeyEvent(keyEvent: KeyEvent): Boolean {
+ return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
+ keyEvent.type,
+ keyEvent.nativeKeyEvent.keyCode
+ ) ?: false
+ }
+
+ data class DialogViewModel(
+ val text: String,
+
+ /** Callback to run after the dialog has been dismissed by the user. */
+ val onDismiss: () -> Unit,
+ )
+
+ data class UserSwitcherDropdownItemViewModel(
+ val icon: Icon,
+ val text: Text,
+ val onClick: () -> Unit,
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): BouncerSceneContentViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
deleted file mode 100644
index e2089bb..0000000
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ /dev/null
@@ -1,439 +0,0 @@
-/*
- * 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.bouncer.ui.viewmodel
-
-import android.app.admin.DevicePolicyManager
-import android.app.admin.DevicePolicyResources
-import android.content.Context
-import android.graphics.Bitmap
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.type
-import androidx.core.graphics.drawable.toBitmap
-import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
-import com.android.compose.animation.scene.UserAction
-import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
-import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
-import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
-import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
-import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.user.ui.viewmodel.UserActionViewModel
-import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
-import dagger.Module
-import dagger.Provides
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.job
-import kotlinx.coroutines.launch
-
-/** Holds UI state and handles user input on bouncer UIs. */
-class BouncerViewModel(
- @Application private val applicationContext: Context,
- @Deprecated("TODO(b/354270224): remove this. Injecting CoroutineScope to view-models is banned")
- @Application
- private val applicationScope: CoroutineScope,
- @Main private val mainDispatcher: CoroutineDispatcher,
- private val bouncerInteractor: BouncerInteractor,
- private val inputMethodInteractor: InputMethodInteractor,
- private val simBouncerInteractor: SimBouncerInteractor,
- private val authenticationInteractor: AuthenticationInteractor,
- private val selectedUserInteractor: SelectedUserInteractor,
- private val devicePolicyManager: DevicePolicyManager,
- bouncerMessageViewModel: BouncerMessageViewModel,
- flags: ComposeBouncerFlags,
- selectedUser: Flow<UserViewModel>,
- users: Flow<List<UserViewModel>>,
- userSwitcherMenu: Flow<List<UserActionViewModel>>,
- actionButton: Flow<BouncerActionButtonModel?>,
-) {
- val selectedUserImage: StateFlow<Bitmap?> =
- selectedUser
- .map { it.image.toBitmap() }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null,
- )
-
- val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- bouncerInteractor.dismissDestination.map { prevScene ->
- mapOf(
- Back to UserActionResult(prevScene),
- Swipe(SwipeDirection.Down) to UserActionResult(prevScene),
- )
- }
-
- val message: BouncerMessageViewModel = bouncerMessageViewModel
-
- val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
- combine(
- users,
- userSwitcherMenu,
- ) { users, actions ->
- users.map { user ->
- UserSwitcherDropdownItemViewModel(
- icon = Icon.Loaded(user.image, contentDescription = null),
- text = user.name,
- onClick = user.onClicked ?: {},
- )
- } +
- actions.map { action ->
- UserSwitcherDropdownItemViewModel(
- icon = Icon.Resource(action.iconResourceId, contentDescription = null),
- text = Text.Resource(action.textResourceId),
- onClick = action.onClicked,
- )
- }
- }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = emptyList(),
- )
-
- val isUserSwitcherVisible: Boolean
- get() = bouncerInteractor.isUserSwitcherVisible
-
- // Handle to the scope of the child ViewModel (stored in [authMethod]).
- private var childViewModelScope: CoroutineScope? = null
-
- /** View-model for the current UI, based on the current authentication method. */
- val authMethodViewModel: StateFlow<AuthMethodBouncerViewModel?> =
- authenticationInteractor.authenticationMethod
- .map(::getChildViewModel)
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null,
- )
-
- /**
- * A message for a dialog to show when the user has attempted the wrong credential too many
- * times and now must wait a while before attempting again.
- *
- * If `null`, the lockout dialog should not be shown.
- */
- private val lockoutDialogMessage = MutableStateFlow<String?>(null)
-
- /**
- * A message for a dialog to show when the user has attempted the wrong credential too many
- * times and their user/profile/device data is at risk of being wiped due to a Device Manager
- * policy.
- *
- * If `null`, the wipe dialog should not be shown.
- */
- private val wipeDialogMessage = MutableStateFlow<String?>(null)
-
- /**
- * Models the dialog to be shown to the user, or `null` if no dialog should be shown.
- *
- * Once the dialog is shown, the UI should call [DialogViewModel.onDismiss] when the user
- * dismisses this dialog.
- */
- val dialogViewModel: StateFlow<DialogViewModel?> =
- combine(wipeDialogMessage, lockoutDialogMessage) { _, _ -> createDialogViewModel() }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = createDialogViewModel(),
- )
-
- /**
- * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
- * be shown.
- */
- val actionButton: StateFlow<BouncerActionButtonModel?> =
- actionButton.stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = null
- )
-
- /**
- * Whether the "side-by-side" layout is supported.
- *
- * When presented on its own, without a user switcher (e.g. not on communal devices like
- * tablets, for example), some authentication method UIs don't do well if they're shown in the
- * side-by-side layout; these need to be shown with the standard layout so they can take up as
- * much width as possible.
- */
- val isSideBySideSupported: StateFlow<Boolean> =
- authMethodViewModel
- .map { authMethod -> isSideBySideSupported(authMethod) }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = isSideBySideSupported(authMethodViewModel.value),
- )
-
- /**
- * Whether the splitting the UI around the fold seam (where the hinge is on a foldable device)
- * is required.
- */
- val isFoldSplitRequired: StateFlow<Boolean> =
- authMethodViewModel
- .map { authMethod -> isFoldSplitRequired(authMethod) }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = isFoldSplitRequired(authMethodViewModel.value),
- )
-
- private val isInputEnabled: StateFlow<Boolean> =
- bouncerMessageViewModel.isLockoutMessagePresent
- .map { lockoutMessagePresent -> !lockoutMessagePresent }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = authenticationInteractor.lockoutEndTimestamp == null,
- )
-
- init {
- if (flags.isComposeBouncerOrSceneContainerEnabled()) {
- // Keeps the upcoming wipe dialog up-to-date.
- applicationScope.launch {
- authenticationInteractor.upcomingWipe.collect { wipeModel ->
- wipeDialogMessage.value = wipeModel?.message
- }
- }
- }
- }
-
- private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
- return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
- }
-
- private fun isFoldSplitRequired(authMethod: AuthMethodBouncerViewModel?): Boolean {
- return authMethod !is PasswordBouncerViewModel
- }
-
- private fun getChildViewModel(
- authenticationMethod: AuthenticationMethodModel,
- ): AuthMethodBouncerViewModel? {
- // If the current child view-model matches the authentication method, reuse it instead of
- // creating a new instance.
- val childViewModel = authMethodViewModel.value
- if (authenticationMethod == childViewModel?.authenticationMethod) {
- return childViewModel
- }
-
- childViewModelScope?.cancel()
- val newViewModelScope = createChildCoroutineScope(applicationScope)
- childViewModelScope = newViewModelScope
- return when (authenticationMethod) {
- is AuthenticationMethodModel.Pin ->
- PinBouncerViewModel(
- applicationContext = applicationContext,
- viewModelScope = newViewModelScope,
- interactor = bouncerInteractor,
- isInputEnabled = isInputEnabled,
- simBouncerInteractor = simBouncerInteractor,
- authenticationMethod = authenticationMethod,
- onIntentionalUserInput = ::onIntentionalUserInput
- )
- is AuthenticationMethodModel.Sim ->
- PinBouncerViewModel(
- applicationContext = applicationContext,
- viewModelScope = newViewModelScope,
- interactor = bouncerInteractor,
- isInputEnabled = isInputEnabled,
- simBouncerInteractor = simBouncerInteractor,
- authenticationMethod = authenticationMethod,
- onIntentionalUserInput = ::onIntentionalUserInput
- )
- is AuthenticationMethodModel.Password ->
- PasswordBouncerViewModel(
- viewModelScope = newViewModelScope,
- isInputEnabled = isInputEnabled,
- interactor = bouncerInteractor,
- inputMethodInteractor = inputMethodInteractor,
- selectedUserInteractor = selectedUserInteractor,
- onIntentionalUserInput = ::onIntentionalUserInput
- )
- is AuthenticationMethodModel.Pattern ->
- PatternBouncerViewModel(
- applicationContext = applicationContext,
- viewModelScope = newViewModelScope,
- interactor = bouncerInteractor,
- isInputEnabled = isInputEnabled,
- onIntentionalUserInput = ::onIntentionalUserInput
- )
- else -> null
- }
- }
-
- private fun onIntentionalUserInput() {
- message.showDefaultMessage()
- bouncerInteractor.onIntentionalUserInput()
- }
-
- private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
- return CoroutineScope(
- SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
- )
- }
-
- /**
- * @return A message warning the user that the user/profile/device will be wiped upon a further
- * [AuthenticationWipeModel.remainingAttempts] unsuccessful authentication attempts.
- */
- private fun AuthenticationWipeModel.getAlmostAtWipeMessage(): String {
- val message =
- applicationContext.getString(
- wipeTarget.messageIdForAlmostWipe,
- failedAttempts,
- remainingAttempts,
- )
- return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
- devicePolicyManager.resources.getString(
- DevicePolicyResources.Strings.SystemUi
- .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
- { message },
- failedAttempts,
- remainingAttempts,
- ) ?: message
- } else {
- message
- }
- }
-
- /**
- * @return A message informing the user that their user/profile/device will be wiped promptly.
- */
- private fun AuthenticationWipeModel.getWipeMessage(): String {
- val message = applicationContext.getString(wipeTarget.messageIdForWipe, failedAttempts)
- return if (wipeTarget == AuthenticationWipeModel.WipeTarget.ManagedProfile) {
- devicePolicyManager.resources.getString(
- DevicePolicyResources.Strings.SystemUi
- .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
- { message },
- failedAttempts,
- ) ?: message
- } else {
- message
- }
- }
-
- private val AuthenticationWipeModel.message: String
- get() = if (remainingAttempts > 0) getAlmostAtWipeMessage() else getWipeMessage()
-
- private fun createDialogViewModel(): DialogViewModel? {
- val wipeText = wipeDialogMessage.value
- val lockoutText = lockoutDialogMessage.value
- return when {
- // The wipe dialog takes priority over the lockout dialog.
- wipeText != null ->
- DialogViewModel(
- text = wipeText,
- onDismiss = { wipeDialogMessage.value = null },
- )
- lockoutText != null ->
- DialogViewModel(
- text = lockoutText,
- onDismiss = { lockoutDialogMessage.value = null },
- )
- else -> null // No dialog to show.
- }
- }
-
- /**
- * Notifies that a key event has occurred.
- *
- * @return `true` when the [KeyEvent] was consumed as user input on bouncer; `false` otherwise.
- */
- fun onKeyEvent(keyEvent: KeyEvent): Boolean {
- return (authMethodViewModel.value as? PinBouncerViewModel)?.onKeyEvent(
- keyEvent.type,
- keyEvent.nativeKeyEvent.keyCode
- ) ?: false
- }
-
- data class DialogViewModel(
- val text: String,
-
- /** Callback to run after the dialog has been dismissed by the user. */
- val onDismiss: () -> Unit,
- )
-
- data class UserSwitcherDropdownItemViewModel(
- val icon: Icon,
- val text: Text,
- val onClick: () -> Unit,
- )
-}
-
-@Module
-object BouncerViewModelModule {
-
- @Provides
- @SysUISingleton
- fun viewModel(
- @Application applicationContext: Context,
- @Application applicationScope: CoroutineScope,
- @Main mainDispatcher: CoroutineDispatcher,
- bouncerInteractor: BouncerInteractor,
- imeInteractor: InputMethodInteractor,
- simBouncerInteractor: SimBouncerInteractor,
- actionButtonInteractor: BouncerActionButtonInteractor,
- authenticationInteractor: AuthenticationInteractor,
- selectedUserInteractor: SelectedUserInteractor,
- flags: ComposeBouncerFlags,
- userSwitcherViewModel: UserSwitcherViewModel,
- devicePolicyManager: DevicePolicyManager,
- bouncerMessageViewModel: BouncerMessageViewModel,
- ): BouncerViewModel {
- return BouncerViewModel(
- applicationContext = applicationContext,
- applicationScope = applicationScope,
- mainDispatcher = mainDispatcher,
- bouncerInteractor = bouncerInteractor,
- inputMethodInteractor = imeInteractor,
- simBouncerInteractor = simBouncerInteractor,
- authenticationInteractor = authenticationInteractor,
- selectedUserInteractor = selectedUserInteractor,
- devicePolicyManager = devicePolicyManager,
- bouncerMessageViewModel = bouncerMessageViewModel,
- flags = flags,
- selectedUser = userSwitcherViewModel.selectedUser,
- users = userSwitcherViewModel.users,
- userSwitcherMenu = userSwitcherViewModel.menu,
- actionButton = actionButtonInteractor.actionButton,
- )
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index 052fb6b..9ead7a0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -23,29 +23,33 @@
import com.android.systemui.res.R
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.kotlin.onSubscriberAdded
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the password bouncer UI. */
-class PasswordBouncerViewModel(
- viewModelScope: CoroutineScope,
- isInputEnabled: StateFlow<Boolean>,
+class PasswordBouncerViewModel
+@AssistedInject
+constructor(
interactor: BouncerInteractor,
- private val onIntentionalUserInput: () -> Unit,
private val inputMethodInteractor: InputMethodInteractor,
private val selectedUserInteractor: SelectedUserInteractor,
+ @Assisted isInputEnabled: StateFlow<Boolean>,
+ @Assisted private val onIntentionalUserInput: () -> Unit,
) :
AuthMethodBouncerViewModel(
- viewModelScope = viewModelScope,
interactor = interactor,
isInputEnabled = isInputEnabled,
) {
@@ -59,28 +63,70 @@
override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message
+ private val _isImeSwitcherButtonVisible = MutableStateFlow(false)
/** Informs the UI whether the input method switcher button should be visible. */
- val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow()
+ val isImeSwitcherButtonVisible: StateFlow<Boolean> = _isImeSwitcherButtonVisible.asStateFlow()
/** Whether the text field element currently has focus. */
private val isTextFieldFocused = MutableStateFlow(false)
+ private val _isTextFieldFocusRequested =
+ MutableStateFlow(isInputEnabled.value && !isTextFieldFocused.value)
/** Whether the UI should request focus on the text field element. */
- val isTextFieldFocusRequested =
- combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus -> hasInput && !hasFocus }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = isInputEnabled.value && !isTextFieldFocused.value,
- )
+ val isTextFieldFocusRequested = _isTextFieldFocusRequested.asStateFlow()
+ private val _selectedUserId = MutableStateFlow(selectedUserInteractor.getSelectedUserId())
/** The ID of the currently-selected user. */
- val selectedUserId: StateFlow<Int> =
- selectedUserInteractor.selectedUser.stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = selectedUserInteractor.getSelectedUserId(),
- )
+ val selectedUserId: StateFlow<Int> = _selectedUserId.asStateFlow()
+
+ private val requests = Channel<Request>(Channel.BUFFERED)
+
+ override suspend fun onActivated() {
+ coroutineScope {
+ launch { super.onActivated() }
+ launch {
+ requests.receiveAsFlow().collect { request ->
+ when (request) {
+ is OnImeSwitcherButtonClicked -> {
+ inputMethodInteractor.showInputMethodPicker(
+ displayId = request.displayId,
+ showAuxiliarySubtypes = false,
+ )
+ }
+ is OnImeDismissed -> {
+ interactor.onImeHiddenByUser()
+ }
+ }
+ }
+ }
+ launch {
+ combine(isInputEnabled, isTextFieldFocused) { hasInput, hasFocus ->
+ hasInput && !hasFocus
+ }
+ .collectLatest { _isTextFieldFocusRequested.value = it }
+ }
+ launch {
+ selectedUserInteractor.selectedUser.collectLatest { _selectedUserId.value = it }
+ }
+ launch {
+ // Re-fetch the currently-enabled IMEs whenever the selected user changes, and
+ // whenever
+ // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
+ combine(
+ // InputMethodManagerService sometimes takes some time to update its
+ // internal
+ // state when the selected user changes. As a workaround, delay fetching the
+ // IME
+ // info.
+ selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
+ _isImeSwitcherButtonVisible.onSubscriberAdded()
+ ) { selectedUserId, _ ->
+ inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
+ }
+ .collectLatest { _isImeSwitcherButtonVisible.value = it }
+ }
+ }
+ }
override fun onHidden() {
super.onHidden()
@@ -106,9 +152,7 @@
/** Notifies that the user clicked the button to change the input method. */
fun onImeSwitcherButtonClicked(displayId: Int) {
- viewModelScope.launch {
- inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false)
- }
+ requests.trySend(OnImeSwitcherButtonClicked(displayId))
}
/** Notifies that the user has pressed the key for attempting to authenticate the password. */
@@ -120,7 +164,7 @@
/** Notifies that the user has dismissed the software keyboard (IME). */
fun onImeDismissed() {
- viewModelScope.launch { interactor.onImeHiddenByUser() }
+ requests.trySend(OnImeDismissed)
}
/** Notifies that the password text field has gained or lost focus. */
@@ -128,34 +172,21 @@
isTextFieldFocused.value = isFocused
}
- /**
- * Whether the input method switcher button should be displayed in the password bouncer UI. The
- * value may be stale at the moment of subscription to this flow, but it is guaranteed to be
- * shortly updated with a fresh value.
- *
- * Note: Each added subscription triggers an IPC call in the background, so this should only be
- * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown).
- */
- private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> {
- val isImeSwitcherButtonVisible = MutableStateFlow(value = false)
- viewModelScope.launch {
- // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever
- // the UI subscribes to the `isImeSwitcherButtonVisible` flow.
- combine(
- // InputMethodManagerService sometimes takes some time to update its internal
- // state when the selected user changes. As a workaround, delay fetching the IME
- // info.
- selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) },
- isImeSwitcherButtonVisible.onSubscriberAdded()
- ) { selectedUserId, _ ->
- inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId)
- }
- .collect { isImeSwitcherButtonVisible.value = it }
- }
- return isImeSwitcherButtonVisible.asStateFlow()
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ isInputEnabled: StateFlow<Boolean>,
+ onIntentionalUserInput: () -> Unit,
+ ): PasswordBouncerViewModel
}
companion object {
@VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds
}
+
+ private sealed interface Request
+
+ private data class OnImeSwitcherButtonClicked(val displayId: Int) : Request
+
+ private data object OnImeDismissed : Request
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 8b9c0a9a..b1df04b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -22,28 +22,32 @@
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the pattern bouncer UI. */
-class PatternBouncerViewModel(
+class PatternBouncerViewModel
+@AssistedInject
+constructor(
private val applicationContext: Context,
- viewModelScope: CoroutineScope,
interactor: BouncerInteractor,
- isInputEnabled: StateFlow<Boolean>,
- private val onIntentionalUserInput: () -> Unit,
+ @Assisted isInputEnabled: StateFlow<Boolean>,
+ @Assisted private val onIntentionalUserInput: () -> Unit,
) :
AuthMethodBouncerViewModel(
- viewModelScope = viewModelScope,
interactor = interactor,
isInputEnabled = isInputEnabled,
) {
@@ -54,17 +58,10 @@
/** The number of rows in the dot grid. */
val rowCount = 3
- private val _selectedDots = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
-
+ private val selectedDotSet = MutableStateFlow<LinkedHashSet<PatternDotViewModel>>(linkedSetOf())
+ private val selectedDotList = MutableStateFlow(selectedDotSet.value.toList())
/** The dots that were selected by the user, in the order of selection. */
- val selectedDots: StateFlow<List<PatternDotViewModel>> =
- _selectedDots
- .map { it.toList() }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = emptyList(),
- )
+ val selectedDots: StateFlow<List<PatternDotViewModel>> = selectedDotList.asStateFlow()
private val _currentDot = MutableStateFlow<PatternDotViewModel?>(null)
@@ -83,6 +80,17 @@
override val lockoutMessageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message
+ override suspend fun onActivated() {
+ coroutineScope {
+ launch { super.onActivated() }
+ launch {
+ selectedDotSet
+ .map { it.toList() }
+ .collectLatest { selectedDotList.value = it.toList() }
+ }
+ }
+ }
+
/** Notifies that the user has started a drag gesture across the dot grid. */
fun onDragStart() {
onIntentionalUserInput()
@@ -120,7 +128,7 @@
}
val hitDot = dots.value.firstOrNull { dot -> dot.x == dotColumn && dot.y == dotRow }
- if (hitDot != null && !_selectedDots.value.contains(hitDot)) {
+ if (hitDot != null && !selectedDotSet.value.contains(hitDot)) {
val skippedOverDots =
currentDot.value?.let { previousDot ->
buildList {
@@ -147,9 +155,9 @@
}
} ?: emptyList()
- _selectedDots.value =
+ selectedDotSet.value =
linkedSetOf<PatternDotViewModel>().apply {
- addAll(_selectedDots.value)
+ addAll(selectedDotSet.value)
addAll(skippedOverDots)
add(hitDot)
}
@@ -172,11 +180,11 @@
override fun clearInput() {
_dots.value = defaultDots()
_currentDot.value = null
- _selectedDots.value = linkedSetOf()
+ selectedDotSet.value = linkedSetOf()
}
override fun getInput(): List<Any> {
- return _selectedDots.value.map(PatternDotViewModel::toCoordinate)
+ return selectedDotSet.value.map(PatternDotViewModel::toCoordinate)
}
private fun defaultDots(): List<PatternDotViewModel> {
@@ -204,6 +212,14 @@
max(min(outValue.float, 1f), MIN_DOT_HIT_FACTOR)
}
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ isInputEnabled: StateFlow<Boolean>,
+ onIntentionalUserInput: () -> Unit,
+ ): PatternBouncerViewModel
+ }
+
companion object {
private const val MIN_DOT_HIT_FACTOR = 0.2f
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index aa447ff..cb36560 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -32,29 +32,34 @@
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
import com.android.systemui.res.R
-import kotlinx.coroutines.CoroutineScope
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the PIN code bouncer UI. */
-class PinBouncerViewModel(
+class PinBouncerViewModel
+@AssistedInject
+constructor(
applicationContext: Context,
- viewModelScope: CoroutineScope,
interactor: BouncerInteractor,
- isInputEnabled: StateFlow<Boolean>,
- private val onIntentionalUserInput: () -> Unit,
private val simBouncerInteractor: SimBouncerInteractor,
- authenticationMethod: AuthenticationMethodModel,
+ @Assisted isInputEnabled: StateFlow<Boolean>,
+ @Assisted private val onIntentionalUserInput: () -> Unit,
+ @Assisted override val authenticationMethod: AuthenticationMethodModel,
) :
AuthMethodBouncerViewModel(
- viewModelScope = viewModelScope,
interactor = interactor,
isInputEnabled = isInputEnabled,
) {
@@ -73,69 +78,89 @@
/** Currently entered pin keys. */
val pinInput: StateFlow<PinInputViewModel> = mutablePinInput
+ private val _hintedPinLength = MutableStateFlow<Int?>(null)
/** The length of the PIN for which we should show a hint. */
- val hintedPinLength: StateFlow<Int?> =
- if (isSimAreaVisible) {
- flowOf(null)
- } else {
- interactor.hintedPinLength
- }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
+ val hintedPinLength: StateFlow<Int?> = _hintedPinLength.asStateFlow()
+ private val _backspaceButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden)
/** Appearance of the backspace button. */
val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> =
- combine(
- mutablePinInput,
- interactor.isAutoConfirmEnabled,
- ) { mutablePinEntries, isAutoConfirmEnabled ->
- computeBackspaceButtonAppearance(
- pinInput = mutablePinEntries,
- isAutoConfirmEnabled = isAutoConfirmEnabled,
- )
- }
- .stateIn(
- scope = viewModelScope,
- // Make sure this is kept as WhileSubscribed or we can run into a bug where the
- // downstream continues to receive old/stale/cached values.
- started = SharingStarted.WhileSubscribed(),
- initialValue = ActionButtonAppearance.Hidden,
- )
+ _backspaceButtonAppearance.asStateFlow()
+ private val _confirmButtonAppearance = MutableStateFlow(ActionButtonAppearance.Hidden)
/** Appearance of the confirm button. */
val confirmButtonAppearance: StateFlow<ActionButtonAppearance> =
- interactor.isAutoConfirmEnabled
- .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = ActionButtonAppearance.Hidden,
- )
-
- override val authenticationMethod: AuthenticationMethodModel = authenticationMethod
+ _confirmButtonAppearance.asStateFlow()
override val lockoutMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message
- init {
- viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
+ private val requests = Channel<Request>(Channel.BUFFERED)
+
+ override suspend fun onActivated() {
+ coroutineScope {
+ launch { super.onActivated() }
+ launch {
+ requests.receiveAsFlow().collect { request ->
+ when (request) {
+ is OnErrorDialogDismissed -> {
+ simBouncerInteractor.onErrorDialogDismissed()
+ }
+ is OnAuthenticateButtonClickedForSim -> {
+ isSimUnlockingDialogVisible.value = true
+ simBouncerInteractor.verifySim(getInput())
+ isSimUnlockingDialogVisible.value = false
+ clearInput()
+ }
+ }
+ }
+ }
+ launch { simBouncerInteractor.subId.collect { onResetSimFlow() } }
+ launch {
+ if (isSimAreaVisible) {
+ flowOf(null)
+ } else {
+ interactor.hintedPinLength
+ }
+ .collectLatest { _hintedPinLength.value = it }
+ }
+ launch {
+ combine(
+ mutablePinInput,
+ interactor.isAutoConfirmEnabled,
+ ) { mutablePinEntries, isAutoConfirmEnabled ->
+ computeBackspaceButtonAppearance(
+ pinInput = mutablePinEntries,
+ isAutoConfirmEnabled = isAutoConfirmEnabled,
+ )
+ }
+ .collectLatest { _backspaceButtonAppearance.value = it }
+ }
+ launch {
+ interactor.isAutoConfirmEnabled
+ .map { if (it) ActionButtonAppearance.Hidden else ActionButtonAppearance.Shown }
+ .collectLatest { _confirmButtonAppearance.value = it }
+ }
+ launch {
+ interactor.isPinEnhancedPrivacyEnabled
+ .map { !it }
+ .collectLatest { _isDigitButtonAnimationEnabled.value = it }
+ }
+ }
}
/** Notifies that the user dismissed the sim pin error dialog. */
fun onErrorDialogDismissed() {
- viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() }
+ requests.trySend(OnErrorDialogDismissed)
}
+ private val _isDigitButtonAnimationEnabled =
+ MutableStateFlow(!interactor.isPinEnhancedPrivacyEnabled.value)
/**
* Whether the digit buttons should be animated when touched. Note that this doesn't affect the
* delete or enter buttons; those should always animate.
*/
val isDigitButtonAnimationEnabled: StateFlow<Boolean> =
- interactor.isPinEnhancedPrivacyEnabled
- .map { !it }
- .stateIn(
- scope = viewModelScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = !interactor.isPinEnhancedPrivacyEnabled.value,
- )
+ _isDigitButtonAnimationEnabled.asStateFlow()
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
@@ -163,19 +188,14 @@
/** Notifies that the user clicked the "enter" button. */
fun onAuthenticateButtonClicked() {
if (authenticationMethod == AuthenticationMethodModel.Sim) {
- viewModelScope.launch {
- isSimUnlockingDialogVisible.value = true
- simBouncerInteractor.verifySim(getInput())
- isSimUnlockingDialogVisible.value = false
- clearInput()
- }
+ requests.trySend(OnAuthenticateButtonClickedForSim)
} else {
tryAuthenticate(useAutoConfirm = false)
}
}
fun onDisableEsimButtonClicked() {
- viewModelScope.launch { simBouncerInteractor.disableEsim() }
+ simBouncerInteractor.disableEsim()
}
/** Resets the sim screen and shows a default message. */
@@ -242,6 +262,21 @@
else -> false
}
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ isInputEnabled: StateFlow<Boolean>,
+ onIntentionalUserInput: () -> Unit,
+ authenticationMethod: AuthenticationMethodModel,
+ ): PinBouncerViewModel
+ }
+
+ private sealed interface Request
+
+ private data object OnErrorDialogDismissed : Request
+
+ private data object OnAuthenticateButtonClickedForSim : Request
}
/** Appearance of pin-pad action buttons. */
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt
new file mode 100644
index 0000000..7453368
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractor.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 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.communal.domain.interactor
+
+import android.app.ActivityManager
+import com.android.systemui.common.usagestats.domain.UsageStatsInteractor
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import com.android.systemui.util.kotlin.race
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Detects activity starts that occur while the communal hub is showing, within a short delay of a
+ * widget interaction occurring. Used for detecting non-activity trampolines which otherwise would
+ * not prompt the user for authentication.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class WidgetTrampolineInteractor
+@Inject
+constructor(
+ private val activityStarter: ActivityStarter,
+ private val systemClock: SystemClock,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val taskStackChangeListeners: TaskStackChangeListeners,
+ private val usageStatsInteractor: UsageStatsInteractor,
+ @CommunalLog logBuffer: LogBuffer,
+) {
+ private companion object {
+ const val TAG = "WidgetTrampolineInteractor"
+ }
+
+ private val logger = Logger(logBuffer, TAG)
+
+ /** Waits for a new task to be moved to the foreground. */
+ private suspend fun waitForNewForegroundTask() = suspendCancellableCoroutine { cont ->
+ val listener =
+ object : TaskStackChangeListener {
+ override fun onTaskMovedToFront(taskInfo: ActivityManager.RunningTaskInfo) {
+ if (!cont.isCompleted) {
+ cont.resume(Unit, null)
+ }
+ }
+ }
+ taskStackChangeListeners.registerTaskStackListener(listener)
+ cont.invokeOnCancellation { taskStackChangeListeners.unregisterTaskStackListener(listener) }
+ }
+
+ /**
+ * Waits for an activity to enter a [ActivityEventModel.Lifecycle.RESUMED] state by periodically
+ * polling the system to see if any activities have started.
+ */
+ private suspend fun waitForActivityStartByPolling(startTime: Long): Boolean {
+ while (true) {
+ val events = usageStatsInteractor.queryActivityEvents(startTime = startTime)
+ if (events.any { event -> event.lifecycle == ActivityEventModel.Lifecycle.RESUMED }) {
+ return true
+ } else {
+ // Poll again in the future to check if an activity started.
+ delay(200.milliseconds)
+ }
+ }
+ }
+
+ /** Waits for a transition away from the hub to occur. */
+ private suspend fun waitForTransitionAwayFromHub() {
+ keyguardTransitionInteractor
+ .isFinishedIn(Scenes.Communal, KeyguardState.GLANCEABLE_HUB)
+ .takeWhile { it }
+ .collect {}
+ }
+
+ private suspend fun waitForActivityStartWhileOnHub(): Boolean {
+ val startTime = systemClock.currentTimeMillis()
+ return try {
+ return withTimeout(1.seconds) {
+ race(
+ {
+ waitForNewForegroundTask()
+ true
+ },
+ { waitForActivityStartByPolling(startTime) },
+ {
+ waitForTransitionAwayFromHub()
+ false
+ },
+ )
+ }
+ } catch (e: TimeoutCancellationException) {
+ false
+ }
+ }
+
+ /**
+ * Checks if an activity starts while on the glanceable hub and dismisses the keyguard if it
+ * does. This can detect activities started due to broadcast trampolines from widgets.
+ */
+ suspend fun waitForActivityStartAndDismissKeyguard() {
+ if (waitForActivityStartWhileOnHub()) {
+ logger.d("Detected trampoline, requesting unlock")
+ activityStarter.dismissKeyguardThenExecute(
+ /* action= */ { false },
+ /* cancel= */ null,
+ /* afterKeyguardGone= */ false
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
index c4edcac..99e3232 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/smartspace/SmartspaceInteractionHandler.kt
@@ -48,7 +48,17 @@
InteractionHandlerDelegate(
communalSceneInteractor,
findViewToAnimate = { view -> view is SmartspaceAppWidgetHostView },
- intentStarter = this::startIntent,
+ intentStarter =
+ object : InteractionHandlerDelegate.IntentStarter {
+ override fun startActivity(
+ intent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions,
+ controller: ActivityTransitionAnimator.Controller?
+ ): Boolean {
+ return startIntent(intent, fillInIntent, activityOptions, controller)
+ }
+ },
logger = Logger(logBuffer, TAG),
)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
index d2029d5..5e21afa 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/util/InteractionHandlerDelegate.kt
@@ -19,6 +19,7 @@
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.Intent
+import android.util.Pair as UtilPair
import android.view.View
import android.widget.RemoteViews
import androidx.core.util.component1
@@ -36,14 +37,28 @@
private val logger: Logger,
) : RemoteViews.InteractionHandler {
- /** Responsible for starting the pending intent for launching activities. */
- fun interface IntentStarter {
- fun startPendingIntent(
+ interface IntentStarter {
+ /** Responsible for starting the pending intent for launching activities. */
+ fun startActivity(
intent: PendingIntent,
fillInIntent: Intent,
activityOptions: ActivityOptions,
controller: ActivityTransitionAnimator.Controller?,
): Boolean
+
+ /** Responsible for starting the pending intent for non-activity launches. */
+ fun startPendingIntent(
+ view: View,
+ pendingIntent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions,
+ ): Boolean {
+ return RemoteViews.startPendingIntent(
+ view,
+ pendingIntent,
+ UtilPair(fillInIntent, activityOptions),
+ )
+ }
}
override fun onInteraction(
@@ -55,7 +70,7 @@
str1 = pendingIntent.toLoggingString()
str2 = pendingIntent.creatorPackage
}
- val launchOptions = response.getLaunchOptions(view)
+ val (fillInIntent, activityOptions) = response.getLaunchOptions(view)
return when {
pendingIntent.isActivity -> {
// Forward the fill-in intent and activity options retrieved from the response
@@ -67,15 +82,15 @@
communalSceneInteractor.setIsLaunchingWidget(true)
CommunalTransitionAnimatorController(it, communalSceneInteractor)
}
- val (fillInIntent, activityOptions) = launchOptions
- intentStarter.startPendingIntent(
+ intentStarter.startActivity(
pendingIntent,
fillInIntent,
activityOptions,
animationController
)
}
- else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions)
+ else ->
+ intentStarter.startPendingIntent(view, pendingIntent, fillInIntent, activityOptions)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
index 0eeb506..121b4a3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt
@@ -21,22 +21,30 @@
import android.content.Intent
import android.view.View
import android.widget.RemoteViews
+import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags.communalWidgetTrampolineFix
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.WidgetTrampolineInteractor
import com.android.systemui.communal.util.InteractionHandlerDelegate
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.plugins.ActivityStarter
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
@SysUISingleton
class WidgetInteractionHandler
@Inject
constructor(
+ @Application applicationScope: CoroutineScope,
private val activityStarter: ActivityStarter,
communalSceneInteractor: CommunalSceneInteractor,
+ private val widgetTrampolineInteractor: WidgetTrampolineInteractor,
@CommunalLog val logBuffer: LogBuffer,
) : RemoteViews.InteractionHandler {
@@ -48,7 +56,52 @@
InteractionHandlerDelegate(
communalSceneInteractor,
findViewToAnimate = { view -> view is CommunalAppWidgetHostView },
- intentStarter = this::startIntent,
+ intentStarter =
+ object : InteractionHandlerDelegate.IntentStarter {
+ private var job: Job? = null
+
+ override fun startActivity(
+ intent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions,
+ controller: ActivityTransitionAnimator.Controller?
+ ): Boolean {
+ cancelTrampolineMonitoring()
+ return startActivityIntent(
+ intent,
+ fillInIntent,
+ activityOptions,
+ controller
+ )
+ }
+
+ override fun startPendingIntent(
+ view: View,
+ pendingIntent: PendingIntent,
+ fillInIntent: Intent,
+ activityOptions: ActivityOptions
+ ): Boolean {
+ cancelTrampolineMonitoring()
+ if (communalWidgetTrampolineFix()) {
+ job =
+ applicationScope.launch("$TAG#monitorForActivityStart") {
+ widgetTrampolineInteractor
+ .waitForActivityStartAndDismissKeyguard()
+ }
+ }
+ return super.startPendingIntent(
+ view,
+ pendingIntent,
+ fillInIntent,
+ activityOptions
+ )
+ }
+
+ private fun cancelTrampolineMonitoring() {
+ job?.cancel()
+ job = null
+ }
+ },
logger = Logger(logBuffer, TAG),
)
@@ -58,7 +111,7 @@
response: RemoteViews.RemoteResponse
): Boolean = delegate.onInteraction(view, pendingIntent, response)
- private fun startIntent(
+ private fun startActivityIntent(
pendingIntent: PendingIntent,
fillInIntent: Intent,
extraOptions: ActivityOptions,
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
index b9b3895..36b9ac7 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
@@ -17,6 +17,7 @@
package com.android.systemui.inputdevice.tutorial.data.repository
import android.content.Context
+import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
@@ -33,15 +34,19 @@
import kotlinx.coroutines.flow.map
@SysUISingleton
-class TutorialSchedulerRepository
-@Inject
-constructor(
- @Application private val applicationContext: Context,
- @Background private val backgroundScope: CoroutineScope
+class TutorialSchedulerRepository(
+ private val applicationContext: Context,
+ backgroundScope: CoroutineScope,
+ dataStoreName: String
) {
+ @Inject
+ constructor(
+ @Application applicationContext: Context,
+ @Background backgroundScope: CoroutineScope
+ ) : this(applicationContext, backgroundScope, dataStoreName = "TutorialScheduler")
private val Context.dataStore: DataStore<Preferences> by
- preferencesDataStore(name = DATASTORE_NAME, scope = backgroundScope)
+ preferencesDataStore(name = dataStoreName, scope = backgroundScope)
suspend fun isLaunched(deviceType: DeviceType): Boolean = loadData()[deviceType]!!.isLaunched
@@ -81,8 +86,12 @@
private fun getConnectKey(device: DeviceType) =
longPreferencesKey(device.name + CONNECT_TIME_SUFFIX)
+ @VisibleForTesting
+ suspend fun clearDataStore() {
+ applicationContext.dataStore.edit { it.clear() }
+ }
+
companion object {
- const val DATASTORE_NAME = "TutorialScheduler"
const val IS_LAUNCHED_SUFFIX = "_IS_LAUNCHED"
const val CONNECT_TIME_SUFFIX = "_CONNECTED_TIME"
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index f178708..04604e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.phone;
-
-
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
@@ -38,11 +36,8 @@
import com.android.systemui.Dependency;
import com.android.systemui.Flags;
import com.android.systemui.Gefingerpoken;
-import com.android.systemui.plugins.DarkIconDispatcher;
-import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
-import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
@@ -55,8 +50,6 @@
private final StatusBarContentInsetsProvider mContentInsetsProvider;
private final StatusBarWindowController mStatusBarWindowController;
- private DarkReceiver mBattery;
- private Clock mClock;
private int mRotationOrientation = -1;
@Nullable
private View mCutoutSpace;
@@ -93,8 +86,6 @@
@Override
public void onFinishInflate() {
super.onFinishInflate();
- mBattery = findViewById(R.id.battery);
- mClock = findViewById(R.id.clock);
mCutoutSpace = findViewById(R.id.cutout_space_view);
updateResources();
@@ -103,9 +94,6 @@
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- // Always have Battery meters in the status bar observe the dark/light modes.
- Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
- Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mClock);
if (updateDisplayParameters()) {
updateLayoutForCutout();
updateWindowHeight();
@@ -115,8 +103,6 @@
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
- Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mClock);
mDisplayCutout = null;
}
@@ -136,10 +122,6 @@
updateWindowHeight();
}
- void onDensityOrFontScaleChanged() {
- mClock.onDensityOrFontScaleChanged();
- }
-
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if (updateDisplayParameters()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index a818c05..468a3c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -23,10 +23,13 @@
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
+import androidx.annotation.VisibleForTesting
import com.android.systemui.Flags
import com.android.systemui.Gefingerpoken
+import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS
+import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.ui.view.WindowRootView
@@ -35,6 +38,7 @@
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
+import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -68,19 +72,27 @@
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController,
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
+ private val darkIconDispatcher: DarkIconDispatcher,
) : ViewController<PhoneStatusBarView>(view) {
+ private lateinit var battery: BatteryMeterView
+ private lateinit var clock: Clock
private lateinit var statusContainer: View
private val configurationListener =
object : ConfigurationController.ConfigurationListener {
override fun onDensityOrFontScaleChanged() {
- mView.onDensityOrFontScaleChanged()
+ clock.onDensityOrFontScaleChanged()
}
}
override fun onViewAttached() {
statusContainer = mView.requireViewById(R.id.system_icons)
+ clock = mView.requireViewById(R.id.clock)
+ battery = mView.requireViewById(R.id.battery)
+
+ addDarkReceivers()
+
statusContainer.setOnHoverListener(
statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)
)
@@ -133,7 +145,9 @@
}
}
- override fun onViewDetached() {
+ @VisibleForTesting
+ public override fun onViewDetached() {
+ removeDarkReceivers()
statusContainer.setOnHoverListener(null)
progressProvider?.setReadyToHandleTransition(false)
moveFromCenterAnimationController?.onViewDetached()
@@ -182,6 +196,16 @@
}
}
+ private fun addDarkReceivers() {
+ darkIconDispatcher.addDarkReceiver(battery)
+ darkIconDispatcher.addDarkReceiver(clock)
+ }
+
+ private fun removeDarkReceivers() {
+ darkIconDispatcher.removeDarkReceiver(battery)
+ darkIconDispatcher.removeDarkReceiver(clock)
+ }
+
inner class PhoneStatusBarViewTouchHandler : Gefingerpoken {
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return if (Flags.statusBarSwipeOverChip()) {
@@ -285,6 +309,7 @@
private val viewUtil: ViewUtil,
private val configurationController: ConfigurationController,
private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
+ private val darkIconDispatcher: DarkIconDispatcher,
) {
fun create(view: PhoneStatusBarView): PhoneStatusBarViewController {
val statusBarMoveFromCenterAnimationController =
@@ -309,6 +334,7 @@
viewUtil,
configurationController,
statusOverlayHoverListenerFactory,
+ darkIconDispatcher,
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index a4936e6..8e215f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -33,9 +33,10 @@
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
-import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModelFactory
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.motion.createSysUiComposeMotionTestRule
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.testKosmos
@@ -81,7 +82,8 @@
private fun BouncerContentUnderTest() {
PlatformTheme {
BouncerContent(
- viewModel = kosmos.bouncerViewModel,
+ viewModel =
+ rememberViewModel { kosmos.bouncerSceneContentViewModelFactory.create() },
layout = BouncerSceneLayout.BESIDE_USER_SWITCHER,
modifier = Modifier.fillMaxSize().testTag("BouncerContent"),
dialogFactory = bouncerDialogFactory
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
index 2948c02..4b61a0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/PatternBouncerTest.kt
@@ -24,14 +24,14 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
-import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.bouncer.ui.viewmodel.patternBouncerViewModelFactory
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.motion.createSysUiComposeMotionTestRule
import com.android.systemui.testKosmos
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.takeWhile
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -51,15 +51,15 @@
@get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos)
- private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
- private val viewModel by lazy {
- PatternBouncerViewModel(
- applicationContext = context,
- viewModelScope = kosmos.testScope.backgroundScope,
- interactor = bouncerInteractor,
+ private val viewModel =
+ kosmos.patternBouncerViewModelFactory.create(
isInputEnabled = MutableStateFlow(true).asStateFlow(),
onIntentionalUserInput = {},
)
+
+ @Before
+ fun setUp() {
+ viewModel.activateIn(motionTestRule.toolkit.testScope)
}
@Composable
diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt
new file mode 100644
index 0000000..7583399
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.inputdevice.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD
+import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TutorialSchedulerRepositoryTest : SysuiTestCase() {
+
+ private lateinit var underTest: TutorialSchedulerRepository
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+
+ @Before
+ fun setup() {
+ underTest =
+ TutorialSchedulerRepository(
+ context,
+ testScope.backgroundScope,
+ "TutorialSchedulerRepositoryTest"
+ )
+ }
+
+ @After
+ fun clear() {
+ testScope.launch { underTest.clearDataStore() }
+ }
+
+ @Test
+ fun initialState() =
+ testScope.runTest {
+ assertThat(underTest.wasEverConnected(KEYBOARD)).isFalse()
+ assertThat(underTest.wasEverConnected(TOUCHPAD)).isFalse()
+ assertThat(underTest.isLaunched(KEYBOARD)).isFalse()
+ assertThat(underTest.isLaunched(TOUCHPAD)).isFalse()
+ }
+
+ @Test
+ fun connectKeyboard() =
+ testScope.runTest {
+ val now = Instant.now().toEpochMilli()
+ underTest.updateConnectTime(KEYBOARD, now)
+
+ assertThat(underTest.wasEverConnected(KEYBOARD)).isTrue()
+ assertThat(underTest.connectTime(KEYBOARD)).isEqualTo(now)
+ assertThat(underTest.wasEverConnected(TOUCHPAD)).isFalse()
+ }
+
+ @Test
+ fun launchKeyboard() =
+ testScope.runTest {
+ underTest.updateLaunch(KEYBOARD)
+
+ assertThat(underTest.isLaunched(KEYBOARD)).isTrue()
+ assertThat(underTest.isLaunched(TOUCHPAD)).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 2d11917..63192f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -36,6 +36,7 @@
import android.animation.Animator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
import android.service.notification.StatusBarNotification;
import android.testing.TestableLooper;
import android.view.MotionEvent;
@@ -52,6 +53,7 @@
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationContentAlphaOptimization;
import org.junit.Before;
import org.junit.Rule;
@@ -672,17 +674,31 @@
}
@Test
- public void testForceResetSwipeStateDoesNothingIfTranslationIsZero() {
+ public void testForceResetSwipeStateDoesNothingIfTranslationIsZeroAndAlphaIsOne() {
doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth();
doReturn(0f).when(mNotificationRow).getTranslationX();
+ doReturn(1f).when(mNotificationRow).getAlpha();
mSwipeHelper.forceResetSwipeState(mNotificationRow);
verify(mNotificationRow).getTranslationX();
+ verify(mNotificationRow).getAlpha();
verifyNoMoreInteractions(mNotificationRow);
}
@Test
+ @EnableFlags(NotificationContentAlphaOptimization.FLAG_NAME)
+ public void testForceResetSwipeStateResetsAlphaIfTranslationIsZeroAndAlphaNotOne() {
+ doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth();
+ doReturn(0f).when(mNotificationRow).getTranslationX();
+ doReturn(0.5f).when(mNotificationRow).getAlpha();
+
+ mSwipeHelper.forceResetSwipeState(mNotificationRow);
+
+ verify(mNotificationRow).setContentAlpha(eq(1f));
+ }
+
+ @Test
public void testForceResetSwipeStateResetsTranslationAndAlpha() {
doReturn(FAKE_ROW_WIDTH).when(mNotificationRow).getMeasuredWidth();
doReturn(10f).when(mNotificationRow).getTranslationX();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 5b45781..30e7247 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -33,8 +33,11 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.systemui.SysuiTestCase
+import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.fakeDarkIconDispatcher
import com.android.systemui.res.R
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeControllerImpl
@@ -42,6 +45,7 @@
import com.android.systemui.shade.ShadeViewController
import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.unfold.SysUIUnfoldComponent
@@ -70,7 +74,9 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class PhoneStatusBarViewControllerTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val fakeDarkIconDispatcher = kosmos.fakeDarkIconDispatcher
@Mock private lateinit var shadeViewController: ShadeViewController
@Mock private lateinit var panelExpansionInteractor: PanelExpansionInteractor
@Mock private lateinit var featureFlags: FeatureFlags
@@ -91,6 +97,12 @@
private lateinit var view: PhoneStatusBarView
private lateinit var controller: PhoneStatusBarViewController
+ private val clockView: Clock
+ get() = view.requireViewById(R.id.clock)
+
+ private val batteryView: BatteryMeterView
+ get() = view.requireViewById(R.id.battery)
+
private val unfoldConfig = UnfoldConfig()
@Before
@@ -114,16 +126,25 @@
@Test
fun onViewAttachedAndDrawn_startListeningConfigurationControllerCallback() {
val view = createViewMock()
- val argumentCaptor =
- ArgumentCaptor.forClass(ConfigurationController.ConfigurationListener::class.java)
+
InstrumentationRegistry.getInstrumentation().runOnMainSync {
controller = createAndInitController(view)
}
- verify(configurationController).addCallback(argumentCaptor.capture())
- argumentCaptor.value.onDensityOrFontScaleChanged()
+ verify(configurationController).addCallback(any())
+ }
- verify(view).onDensityOrFontScaleChanged()
+ @Test
+ fun onViewAttachedAndDrawn_darkReceiversRegistered() {
+ val view = createViewMock()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ controller = createAndInitController(view)
+ }
+
+ assertThat(fakeDarkIconDispatcher.receivers.size).isEqualTo(2)
+ assertThat(fakeDarkIconDispatcher.receivers).contains(clockView)
+ assertThat(fakeDarkIconDispatcher.receivers).contains(batteryView)
}
@Test
@@ -158,6 +179,21 @@
}
@Test
+ fun onViewDetached_darkReceiversUnregistered() {
+ val view = createViewMock()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ controller = createAndInitController(view)
+ }
+
+ assertThat(fakeDarkIconDispatcher.receivers).isNotEmpty()
+
+ controller.onViewDetached()
+
+ assertThat(fakeDarkIconDispatcher.receivers).isEmpty()
+ }
+
+ @Test
fun handleTouchEventFromStatusBar_panelsNotEnabled_returnsFalseAndNoViewEvent() {
`when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(false)
val returnVal =
@@ -353,7 +389,8 @@
shadeLogger,
viewUtil,
configurationController,
- mStatusOverlayHoverListenerFactory
+ mStatusOverlayHoverListenerFactory,
+ fakeDarkIconDispatcher,
)
.create(view)
.also { it.init() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index abc50bc..ed5ec7b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -35,7 +35,6 @@
import com.android.systemui.Flags.FLAG_STATUS_BAR_SWIPE_OVER_CHIP
import com.android.systemui.Gefingerpoken
import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.res.R
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.mockito.mock
@@ -64,7 +63,6 @@
StatusBarContentInsetsProvider::class.java,
contentInsetsProvider
)
- mDependency.injectTestDependency(DarkIconDispatcher::class.java, mock<DarkIconDispatcher>())
mDependency.injectTestDependency(StatusBarWindowController::class.java, windowController)
context.ensureTestableResources()
view = spy(createStatusBarView())
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
index e70631e..e8612d08 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.bouncer.ui.viewmodel
import android.content.applicationContext
@@ -26,26 +28,31 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
import com.android.systemui.util.time.systemClock
import kotlinx.coroutines.ExperimentalCoroutinesApi
-@ExperimentalCoroutinesApi
-val Kosmos.bouncerMessageViewModel by
- Kosmos.Fixture {
- BouncerMessageViewModel(
- applicationContext = applicationContext,
- applicationScope = testScope.backgroundScope,
- bouncerInteractor = bouncerInteractor,
- simBouncerInteractor = simBouncerInteractor,
- authenticationInteractor = authenticationInteractor,
- selectedUser = userSwitcherViewModel.selectedUser,
- clock = systemClock,
- biometricMessageInteractor = biometricMessageInteractor,
- faceAuthInteractor = deviceEntryFaceAuthInteractor,
- deviceUnlockedInteractor = deviceUnlockedInteractor,
- deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor,
- flags = composeBouncerFlags,
- )
+val Kosmos.bouncerMessageViewModel by Fixture {
+ BouncerMessageViewModel(
+ applicationContext = applicationContext,
+ bouncerInteractor = bouncerInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ authenticationInteractor = authenticationInteractor,
+ userSwitcherViewModel = userSwitcherViewModel,
+ clock = systemClock,
+ biometricMessageInteractor = biometricMessageInteractor,
+ faceAuthInteractor = deviceEntryFaceAuthInteractor,
+ deviceUnlockedInteractor = deviceUnlockedInteractor,
+ deviceEntryBiometricsAllowedInteractor = deviceEntryBiometricsAllowedInteractor,
+ flags = composeBouncerFlags,
+ )
+}
+
+val Kosmos.bouncerMessageViewModelFactory by Fixture {
+ object : BouncerMessageViewModel.Factory {
+ override fun create(): BouncerMessageViewModel {
+ return bouncerMessageViewModel
+ }
}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index c3dad74..e405d17 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -21,6 +21,7 @@
import android.app.admin.devicePolicyManager
import android.content.applicationContext
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
@@ -28,28 +29,97 @@
import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.kosmos.testScope
import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.StateFlow
-val Kosmos.bouncerViewModel by Fixture {
- BouncerViewModel(
- applicationContext = applicationContext,
- applicationScope = testScope.backgroundScope,
- mainDispatcher = testDispatcher,
+val Kosmos.bouncerSceneActionsViewModel by Fixture {
+ BouncerSceneActionsViewModel(
bouncerInteractor = bouncerInteractor,
- inputMethodInteractor = inputMethodInteractor,
- simBouncerInteractor = simBouncerInteractor,
- authenticationInteractor = authenticationInteractor,
- selectedUserInteractor = selectedUserInteractor,
- devicePolicyManager = devicePolicyManager,
- bouncerMessageViewModel = bouncerMessageViewModel,
- flags = composeBouncerFlags,
- selectedUser = userSwitcherViewModel.selectedUser,
- users = userSwitcherViewModel.users,
- userSwitcherMenu = userSwitcherViewModel.menu,
- actionButton = bouncerActionButtonInteractor.actionButton,
)
}
+
+val Kosmos.bouncerSceneActionsViewModelFactory by Fixture {
+ object : BouncerSceneActionsViewModel.Factory {
+ override fun create(): BouncerSceneActionsViewModel {
+ return bouncerSceneActionsViewModel
+ }
+ }
+}
+
+val Kosmos.bouncerSceneContentViewModel by Fixture {
+ BouncerSceneContentViewModel(
+ applicationContext = applicationContext,
+ bouncerInteractor = bouncerInteractor,
+ authenticationInteractor = authenticationInteractor,
+ devicePolicyManager = devicePolicyManager,
+ bouncerMessageViewModelFactory = bouncerMessageViewModelFactory,
+ flags = composeBouncerFlags,
+ userSwitcher = userSwitcherViewModel,
+ actionButtonInteractor = bouncerActionButtonInteractor,
+ pinViewModelFactory = pinBouncerViewModelFactory,
+ patternViewModelFactory = patternBouncerViewModelFactory,
+ passwordViewModelFactory = passwordBouncerViewModelFactory,
+ )
+}
+
+val Kosmos.bouncerSceneContentViewModelFactory by Fixture {
+ object : BouncerSceneContentViewModel.Factory {
+ override fun create(): BouncerSceneContentViewModel {
+ return bouncerSceneContentViewModel
+ }
+ }
+}
+
+val Kosmos.pinBouncerViewModelFactory by Fixture {
+ object : PinBouncerViewModel.Factory {
+ override fun create(
+ isInputEnabled: StateFlow<Boolean>,
+ onIntentionalUserInput: () -> Unit,
+ authenticationMethod: AuthenticationMethodModel,
+ ): PinBouncerViewModel {
+ return PinBouncerViewModel(
+ applicationContext = applicationContext,
+ interactor = bouncerInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ isInputEnabled = isInputEnabled,
+ onIntentionalUserInput = onIntentionalUserInput,
+ authenticationMethod = authenticationMethod,
+ )
+ }
+ }
+}
+
+val Kosmos.patternBouncerViewModelFactory by Fixture {
+ object : PatternBouncerViewModel.Factory {
+ override fun create(
+ isInputEnabled: StateFlow<Boolean>,
+ onIntentionalUserInput: () -> Unit,
+ ): PatternBouncerViewModel {
+ return PatternBouncerViewModel(
+ applicationContext = applicationContext,
+ interactor = bouncerInteractor,
+ isInputEnabled = isInputEnabled,
+ onIntentionalUserInput = onIntentionalUserInput,
+ )
+ }
+ }
+}
+
+val Kosmos.passwordBouncerViewModelFactory by Fixture {
+ object : PasswordBouncerViewModel.Factory {
+ override fun create(
+ isInputEnabled: StateFlow<Boolean>,
+ onIntentionalUserInput: () -> Unit,
+ ): PasswordBouncerViewModel {
+ return PasswordBouncerViewModel(
+ interactor = bouncerInteractor,
+ inputMethodInteractor = inputMethodInteractor,
+ selectedUserInteractor = selectedUserInteractor,
+ isInputEnabled = isInputEnabled,
+ onIntentionalUserInput = onIntentionalUserInput,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt
new file mode 100644
index 0000000..8124224
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/WidgetTrampolineInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.communal.domain.interactor
+
+import com.android.systemui.common.usagestats.domain.interactor.usageStatsInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.util.time.fakeSystemClock
+
+val Kosmos.widgetTrampolineInteractor: WidgetTrampolineInteractor by
+ Kosmos.Fixture {
+ WidgetTrampolineInteractor(
+ activityStarter = activityStarter,
+ systemClock = fakeSystemClock,
+ keyguardTransitionInteractor = keyguardTransitionInteractor,
+ taskStackChangeListeners = taskStackChangeListeners,
+ usageStatsInteractor = usageStatsInteractor,
+ logBuffer = logcatLogBuffer("WidgetTrampolineInteractor"),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/DarkIconDispatcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/DarkIconDispatcherKosmos.kt
new file mode 100644
index 0000000..3d125e9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/DarkIconDispatcherKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.plugins
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.fakeDarkIconDispatcher: FakeDarkIconDispatcher by
+ Kosmos.Fixture { FakeDarkIconDispatcher() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeDarkIconDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeDarkIconDispatcher.kt
new file mode 100644
index 0000000..102a853
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeDarkIconDispatcher.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.plugins
+
+import android.graphics.Rect
+import java.util.ArrayList
+
+class FakeDarkIconDispatcher : DarkIconDispatcher {
+ val receivers = mutableListOf<DarkIconDispatcher.DarkReceiver>()
+
+ override fun setIconsDarkArea(r: ArrayList<Rect>) {}
+
+ override fun addDarkReceiver(receiver: DarkIconDispatcher.DarkReceiver) {
+ receivers.add(receiver)
+ }
+
+ override fun removeDarkReceiver(receiver: DarkIconDispatcher.DarkReceiver) {
+ receivers.remove(receiver)
+ }
+
+ override fun applyDark(`object`: DarkIconDispatcher.DarkReceiver) {}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt
new file mode 100644
index 0000000..67f611a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/system/TaskStackChangeListenersKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.shared.system
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.taskStackChangeListeners: TaskStackChangeListeners by
+ Kosmos.Fixture { TaskStackChangeListeners.getTestInstance() }
diff --git a/services/contentcapture/java/com/android/server/contentprotection/OWNERS b/services/contentcapture/java/com/android/server/contentprotection/OWNERS
new file mode 100644
index 0000000..3d09da3
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentprotection/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1040349
+
+include /core/java/android/view/contentprotection/OWNERS
+
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 9818916..abb2132 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -489,6 +489,7 @@
} else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
hasValidSound = false;
+ hasValidVibrate = false;
}
}
}
@@ -753,6 +754,13 @@
// notifying app does not have the VIBRATE permission.
final long identity = Binder.clearCallingIdentity();
try {
+ // Need to explicitly cancel a previously playing vibration
+ // Otherwise a looping vibration will not be stopped when starting a new one.
+ if (mVibrateNotificationKey != null
+ && !mVibrateNotificationKey.equals(record.getKey())) {
+ mVibrateNotificationKey = null;
+ mVibratorHelper.cancelVibration();
+ }
final float scale = getVibrationIntensity(record);
final VibrationEffect scaledEffect = Float.compare(scale, DEFAULT_VOLUME) != 0
? mVibratorHelper.scale(effect, scale) : effect;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5bb4a8a..121ab2c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -3771,6 +3771,7 @@
// Shell calls back into Core with the entry bounds to be applied with startWCT.
final Transition enterPipTransition = new Transition(TRANSIT_PIP,
0 /* flags */, getTransitionController(), mWindowManager.mSyncEngine);
+ r.setPictureInPictureParams(params);
enterPipTransition.setPipActivity(r);
r.mAutoEnteringPip = isAutoEnter;
getTransitionController().startCollectOrQueue(enterPipTransition, (deferred) -> {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3f0c9fd..4340771 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4705,6 +4705,15 @@
// it does not follow the ActivityStarter path.
if (topActivity.shouldBeVisible()) {
mAtmService.resumeAppSwitches();
+ // In pip1, when expanding pip to full-screen, the "behind" task is not
+ // actually becoming invisible since task windowing mode is pinned.
+ if (!isPip2ExperimentEnabled) {
+ final ActivityRecord ar = mAtmService.mLastResumedActivity;
+ if (ar != null && ar.getTask() != null) {
+ mAtmService.takeTaskSnapshot(ar.getTask().mTaskId,
+ true /* updateCache */);
+ }
+ }
}
} else if (isPip2ExperimentEnabled) {
super.setWindowingMode(windowingMode);
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS b/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS
index 24561c5..3d09da3 100644
--- a/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/OWNERS
@@ -1,3 +1,4 @@
-# Bug component: 544200
+# Bug component: 1040349
-include /core/java/android/view/contentcapture/OWNERS
+include /core/java/android/view/contentprotection/OWNERS
+
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 643ee4a..62e5b9a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2007,6 +2007,25 @@
}
@Test
+ public void testCanInterruptNonRingtoneInsistentBuzzWithOtherBuzzyNotification() {
+ NotificationRecord r = getInsistentBuzzyNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyVibrateLooped();
+ assertTrue(r.isInterruptive());
+ assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+ Mockito.reset(mVibrator);
+
+ // New buzzy notification stops previous looping vibration
+ NotificationRecord interrupter = getBuzzyOtherNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(interrupter, DEFAULT_SIGNALS);
+ verifyStopVibrate();
+ // And then vibrates itself
+ verifyVibrate(1);
+ assertTrue(interrupter.isInterruptive());
+ assertNotEquals(-1, interrupter.getLastAudiblyAlertedMs());
+ }
+
+ @Test
public void testRingtoneInsistentBeep_doesNotBlockFutureSoundsOnceStopped() throws Exception {
NotificationChannel ringtoneChannel =
new NotificationChannel("ringtone", "", IMPORTANCE_HIGH);
diff --git a/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl b/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl
index f981fb1..5f0d986 100644
--- a/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl
+++ b/telephony/java/android/telephony/satellite/ISatelliteProvisionStateCallback.aidl
@@ -16,6 +16,8 @@
package android.telephony.satellite;
+import android.telephony.satellite.SatelliteSubscriberProvisionStatus;
+
/**
* Interface for satellite provision state callback.
* @hide
@@ -27,4 +29,14 @@
* @param provisioned True means the service is provisioned and false means it is not.
*/
void onSatelliteProvisionStateChanged(in boolean provisioned);
+
+ /**
+ * Called when the provisioning state of one or more SatelliteSubscriberInfos changes.
+ *
+ * @param satelliteSubscriberProvisionStatus The List contains the latest provisioning states of
+ * the SatelliteSubscriberInfos.
+ * @hide
+ */
+ void onSatelliteSubscriptionProvisionStateChanged(in List<SatelliteSubscriberProvisionStatus>
+ satelliteSubscriberProvisionStatus);
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index e657d7f..0c98327 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1404,6 +1404,16 @@
() -> callback.onSatelliteProvisionStateChanged(
provisioned)));
}
+
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ @Override
+ public void onSatelliteSubscriptionProvisionStateChanged(
+ @NonNull List<SatelliteSubscriberProvisionStatus>
+ satelliteSubscriberProvisionStatus) {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onSatelliteSubscriptionProvisionStateChanged(
+ satelliteSubscriberProvisionStatus)));
+ }
};
sSatelliteProvisionStateCallbackMap.put(callback, internalCallback);
return telephony.registerForSatelliteProvisionStateChanged(
diff --git a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
index a12952b..e8ae0f5 100644
--- a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
+++ b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java
@@ -17,10 +17,13 @@
package android.telephony.satellite;
import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.SystemApi;
import com.android.internal.telephony.flags.Flags;
+import java.util.List;
+
/**
* A callback class for monitoring satellite provision state change events.
*
@@ -39,4 +42,16 @@
*/
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
void onSatelliteProvisionStateChanged(boolean provisioned);
+
+ /**
+ * Called when the provisioning state of one or more SatelliteSubscriberInfos changes.
+ *
+ * @param satelliteSubscriberProvisionStatus The List contains the latest provisioning states
+ * of the SatelliteSubscriberInfos.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ default void onSatelliteSubscriptionProvisionStateChanged(
+ @NonNull List<SatelliteSubscriberProvisionStatus>
+ satelliteSubscriberProvisionStatus) {};
}
diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.aidl b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.aidl
new file mode 100644
index 0000000..80de779
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, 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 android.telephony.satellite;
+
+parcelable SatelliteSubscriberProvisionStatus;
\ No newline at end of file
diff --git a/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
new file mode 100644
index 0000000..e3d619e
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberProvisionStatus.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 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 android.telephony.satellite;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.telephony.flags.Flags;
+
+import java.util.Objects;
+
+/**
+ * Represents the provisioning state of SatelliteSubscriberInfo.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+public class SatelliteSubscriberProvisionStatus implements Parcelable {
+ private SatelliteSubscriberInfo mSubscriberInfo;
+ /** {@code true} mean the satellite subscriber is provisioned, {@code false} otherwise. */
+ private boolean mProvisionStatus;
+
+ public SatelliteSubscriberProvisionStatus(@NonNull Builder builder) {
+ mSubscriberInfo = builder.mSubscriberInfo;
+ mProvisionStatus = builder.mProvisionStatus;
+ }
+
+ /**
+ * Builder class for constructing SatelliteSubscriberProvisionStatus objects
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public static class Builder {
+ private SatelliteSubscriberInfo mSubscriberInfo;
+ private boolean mProvisionStatus;
+
+ /**
+ * Set the SatelliteSubscriberInfo and returns the Builder class.
+ * @hide
+ */
+ public Builder setSatelliteSubscriberInfo(SatelliteSubscriberInfo satelliteSubscriberInfo) {
+ mSubscriberInfo = satelliteSubscriberInfo;
+ return this;
+ }
+
+ /**
+ * Set the SatelliteSubscriberInfo's provisionStatus and returns the Builder class.
+ * @hide
+ */
+ @NonNull
+ public Builder setProvisionStatus(boolean provisionStatus) {
+ mProvisionStatus = provisionStatus;
+ return this;
+ }
+
+ /**
+ * Returns SatelliteSubscriberProvisionStatus object.
+ * @hide
+ */
+ @NonNull
+ public SatelliteSubscriberProvisionStatus build() {
+ return new SatelliteSubscriberProvisionStatus(this);
+ }
+ }
+
+ private SatelliteSubscriberProvisionStatus(Parcel in) {
+ readFromParcel(in);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ mSubscriberInfo.writeToParcel(out, flags);
+ out.writeBoolean(mProvisionStatus);
+ }
+
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public static final @android.annotation.NonNull Creator<SatelliteSubscriberProvisionStatus>
+ CREATOR =
+ new Creator<SatelliteSubscriberProvisionStatus>() {
+ @Override
+ public SatelliteSubscriberProvisionStatus createFromParcel(Parcel in) {
+ return new SatelliteSubscriberProvisionStatus(in);
+ }
+
+ @Override
+ public SatelliteSubscriberProvisionStatus[] newArray(int size) {
+ return new SatelliteSubscriberProvisionStatus[size];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * SatelliteSubscriberInfo that has a provisioning state.
+ * @return SatelliteSubscriberInfo.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public @NonNull SatelliteSubscriberInfo getSatelliteSubscriberInfo() {
+ return mSubscriberInfo;
+ }
+
+ /**
+ * SatelliteSubscriberInfo's provisioning state.
+ * @return {@code true} means provisioning. {@code false} means deprovisioning.
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public @NonNull boolean getProvisionStatus() {
+ return mProvisionStatus;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("SatelliteSubscriberInfo:");
+ sb.append(mSubscriberInfo);
+ sb.append(",");
+
+ sb.append("ProvisionStatus:");
+ sb.append(mProvisionStatus);
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSubscriberInfo, mProvisionStatus);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SatelliteSubscriberProvisionStatus)) return false;
+ SatelliteSubscriberProvisionStatus that = (SatelliteSubscriberProvisionStatus) o;
+ return Objects.equals(mSubscriberInfo, that.mSubscriberInfo)
+ && mProvisionStatus == that.mProvisionStatus;
+ }
+
+ private void readFromParcel(Parcel in) {
+ mSubscriberInfo = in.readParcelable(SatelliteSubscriberInfo.class.getClassLoader(),
+ SatelliteSubscriberInfo.class);
+ mProvisionStatus = in.readBoolean();
+ }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
index 5a48327..9657225 100644
--- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java
@@ -214,6 +214,13 @@
verify(mReader, never()).getViewerString(anyLong());
}
+ @Test
+ public void loadViewerConfigOnLogcatGroupRegistration() {
+ TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
+ mProtoLog.registerGroups(TestProtoLogGroup.TEST_GROUP);
+ verify(mReader).loadViewerConfig(any(), any());
+ }
+
private static class ProtoLogData {
Long mMessageHash = null;
Long mElapsedTime = null;
diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java
index 359eb35..5012c23 100644
--- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java
+++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java
@@ -84,6 +84,7 @@
content.addView(enableSyncButton,
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM));
+ content.setFitsSystemWindows(true);
setContentView(content);
mSv.setZOrderOnTop(false);
diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java
index 73e0163..4119ea2 100644
--- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java
+++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java
@@ -37,6 +37,7 @@
protected void onCreate(Bundle savedInstanceState) {
FrameLayout content = new FrameLayout(this);
+ content.setFitsSystemWindows(true);
super.onCreate(savedInstanceState);
mView = new SurfaceView(this);
content.addView(mView, new FrameLayout.LayoutParams(
diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java
index ac7dc9e..5287068 100644
--- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java
+++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java
@@ -88,6 +88,7 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout content = new LinearLayout(this);
+ content.setFitsSystemWindows(true);
mLocalSurfaceView = new SurfaceView(this);
content.addView(mLocalSurfaceView, new LinearLayout.LayoutParams(
500, 500, Gravity.CENTER_HORIZONTAL | Gravity.TOP));