Merge "Update fp => lock icon transition to NOT be reversible" into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 11da20a..159c17e 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -215,111 +215,37 @@
java_library {
name: "services.core.ravenwood-jarjar",
+ defaults: ["ravenwood-internal-only-visibility-java"],
installable: false,
static_libs: [
"services.core.ravenwood",
],
jarjar_rules: ":ravenwood-services-jarjar-rules",
- visibility: ["//visibility:private"],
-}
-
-java_library {
- name: "services.fakes.ravenwood-jarjar",
- installable: false,
- srcs: [":services.fakes-sources"],
- libs: [
- "ravenwood-framework",
- "services.core.ravenwood",
- ],
- jarjar_rules: ":ravenwood-services-jarjar-rules",
- visibility: ["//visibility:private"],
-}
-
-java_library {
- name: "mockito-ravenwood-prebuilt",
- installable: false,
- static_libs: [
- "mockito-robolectric-prebuilt",
- ],
-}
-
-java_library {
- name: "inline-mockito-ravenwood-prebuilt",
- installable: false,
- static_libs: [
- "inline-mockito-robolectric-prebuilt",
- ],
}
// Jars in "ravenwood-runtime" are set to the classpath, sorted alphabetically.
// Rename some of the dependencies to make sure they're included in the intended order.
java_genrule {
name: "100-framework-minus-apex.ravenwood",
+ defaults: ["ravenwood-internal-only-visibility-genrule"],
cmd: "cp $(in) $(out)",
srcs: [":framework-minus-apex.ravenwood"],
out: ["100-framework-minus-apex.ravenwood.jar"],
- visibility: ["//visibility:private"],
}
java_genrule {
// Use 200 to make sure it comes before the mainline stub ("all-updatable...").
name: "200-kxml2-android",
+ defaults: ["ravenwood-internal-only-visibility-genrule"],
cmd: "cp $(in) $(out)",
srcs: [":kxml2-android"],
out: ["200-kxml2-android.jar"],
- visibility: ["//visibility:private"],
}
java_genrule {
name: "z00-all-updatable-modules-system-stubs",
+ defaults: ["ravenwood-internal-only-visibility-genrule"],
cmd: "cp $(in) $(out)",
srcs: [":all-updatable-modules-system-stubs"],
out: ["z00-all-updatable-modules-system-stubs.jar"],
- visibility: ["//visibility:private"],
-}
-
-android_ravenwood_libgroup {
- name: "ravenwood-runtime",
- libs: [
- "100-framework-minus-apex.ravenwood",
- "200-kxml2-android",
-
- "ravenwood-runtime-common-ravenwood",
-
- "android.test.mock.ravenwood",
- "ravenwood-helper-runtime",
- "hoststubgen-helper-runtime.ravenwood",
- "services.core.ravenwood-jarjar",
- "services.fakes.ravenwood-jarjar",
-
- // Provide runtime versions of utils linked in below
- "junit",
- "truth",
- "flag-junit",
- "ravenwood-framework",
- "ravenwood-junit-impl",
- "ravenwood-junit-impl-flag",
- "mockito-ravenwood-prebuilt",
- "inline-mockito-ravenwood-prebuilt",
-
- // It's a stub, so it should be towards the end.
- "z00-all-updatable-modules-system-stubs",
- ],
- jni_libs: [
- "libandroid_runtime",
- "libravenwood_runtime",
- ],
-}
-
-android_ravenwood_libgroup {
- name: "ravenwood-utils",
- libs: [
- "junit",
- "truth",
- "flag-junit",
- "ravenwood-framework",
- "ravenwood-junit",
- "mockito-ravenwood-prebuilt",
- "inline-mockito-ravenwood-prebuilt",
- ],
}
diff --git a/SQLITE_OWNERS b/SQLITE_OWNERS
index 1ff72e7..783a0b6 100644
--- a/SQLITE_OWNERS
+++ b/SQLITE_OWNERS
@@ -1,2 +1,9 @@
+# Android platform SQLite owners are responsible for:
+# 1. Periodically updating libsqlite from upstream sqlite.org.
+# 2. Escalating libsqlite bug reports to upstream sqlite.org.
+# 3. Addressing bugs, performance regressions, and feature requests
+# in Android SDK SQLite wrappers (android.database.sqlite.*).
+# 4. Reviewing proposed changes to said Android SDK SQLite wrappers.
+
shayba@google.com
shombert@google.com
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5393475..9e2872f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11471,6 +11471,19 @@
}
+package android.os.vibrator.persistence {
+
+ @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
+ method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
+ }
+
+ @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class VibrationXmlParser {
+ method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.vibrator.persistence.ParsedVibration parse(@NonNull java.io.InputStream) throws java.io.IOException;
+ method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.InputStream) throws java.io.IOException;
+ }
+
+}
+
package android.permission {
public final class AdminPermissionControlParams implements android.os.Parcelable {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index d899511..1352465 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2761,21 +2761,24 @@
package android.os.vibrator.persistence {
- public class ParsedVibration {
- method @NonNull public java.util.List<android.os.VibrationEffect> getVibrationEffects();
- method @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
+ @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
+ ctor public ParsedVibration(@NonNull java.util.List<android.os.VibrationEffect>);
+ method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
}
- public final class VibrationXmlParser {
- method @Nullable public static android.os.vibrator.persistence.ParsedVibration parseDocument(@NonNull java.io.Reader) throws java.io.IOException;
- method @Nullable public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.Reader) throws java.io.IOException;
+ @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class VibrationXmlParser {
+ method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.vibrator.persistence.ParsedVibration parse(@NonNull java.io.InputStream) throws java.io.IOException;
+ method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.InputStream) throws java.io.IOException;
+ }
+
+ public static final class VibrationXmlParser.ParseFailedException extends java.io.IOException {
}
public final class VibrationXmlSerializer {
- method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException, android.os.vibrator.persistence.VibrationXmlSerializer.SerializationFailedException;
+ method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException;
}
- public static final class VibrationXmlSerializer.SerializationFailedException extends java.lang.RuntimeException {
+ public static final class VibrationXmlSerializer.SerializationFailedException extends java.io.IOException {
}
}
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 5956e2b..80764af 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -1128,7 +1128,7 @@
private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
mGetPackagesForUidCache =
new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
- 32, CACHE_KEY_PACKAGES_FOR_UID_PROPERTY) {
+ 1024, CACHE_KEY_PACKAGES_FOR_UID_PROPERTY) {
@Override
public GetPackagesForUidResult recompute(Integer uid) {
try {
diff --git a/core/java/android/app/compat/ChangeIdStateCache.java b/core/java/android/app/compat/ChangeIdStateCache.java
index dea4e9c8..7948cec 100644
--- a/core/java/android/app/compat/ChangeIdStateCache.java
+++ b/core/java/android/app/compat/ChangeIdStateCache.java
@@ -32,7 +32,7 @@
public final class ChangeIdStateCache
extends PropertyInvalidatedCache<ChangeIdStateQuery, Boolean> {
private static final String CACHE_KEY = "cache_key.is_compat_change_enabled";
- private static final int MAX_ENTRIES = 64;
+ private static final int MAX_ENTRIES = 2048;
private static boolean sDisabled = false;
private volatile IPlatformCompat mPlatformCompat;
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 282ede3..8c56a9d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -11436,7 +11436,7 @@
private static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
sApplicationInfoCache =
new PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>(
- 32, PermissionManager.CACHE_KEY_PACKAGE_INFO,
+ 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO,
"getApplicationInfo") {
@Override
public ApplicationInfo recompute(ApplicationInfoQuery query) {
@@ -11537,7 +11537,7 @@
private static final PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>
sPackageInfoCache =
new PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>(
- 64, PermissionManager.CACHE_KEY_PACKAGE_INFO,
+ 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO,
"getPackageInfo") {
@Override
public PackageInfo recompute(PackageInfoQuery query) {
diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java
index b98a0d8..c521b96 100644
--- a/core/java/android/credentials/selection/IntentFactory.java
+++ b/core/java/android/credentials/selection/IntentFactory.java
@@ -232,7 +232,17 @@
oemComponentName,
PackageManager.ComponentInfoFlags.of(
PackageManager.MATCH_SYSTEM_ONLY));
- if (info.enabled && info.exported) {
+ boolean oemComponentEnabled = info.enabled;
+ int runtimeComponentEnabledState = context.getPackageManager()
+ .getComponentEnabledSetting(oemComponentName);
+ if (runtimeComponentEnabledState == PackageManager
+ .COMPONENT_ENABLED_STATE_ENABLED) {
+ oemComponentEnabled = true;
+ } else if (runtimeComponentEnabledState == PackageManager
+ .COMPONENT_ENABLED_STATE_DISABLED) {
+ oemComponentEnabled = false;
+ }
+ if (oemComponentEnabled && info.exported) {
intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
.OemUiUsageStatus.SUCCESS);
Slog.i(TAG,
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index b2dcf90..91caedc 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -740,6 +740,12 @@
*/
void onBlockingScreenOn(Runnable unblocker);
+ /**
+ * Called while display is turning to screen state other than state ON to notify that any
+ * pending work from the previous blockScreenOn call should have been cancelled.
+ */
+ void cancelBlockScreenOn();
+
/** Whether auto brightness update in doze is allowed */
boolean allowAutoBrightnessInDoze();
}
@@ -774,6 +780,12 @@
boolean blockScreenOn(Runnable unblocker);
/**
+ * Called while display is turning to screen state other than state ON to notify that any
+ * pending work from the previous blockScreenOn call should have been cancelled.
+ */
+ void cancelBlockScreenOn();
+
+ /**
* Get the brightness levels used to determine automatic brightness based on lux levels.
* @param mode The auto-brightness mode
* (AutomaticBrightnessController.AutomaticBrightnessMode)
diff --git a/core/java/android/os/SharedMemory.java b/core/java/android/os/SharedMemory.java
index d008034..cba4423 100644
--- a/core/java/android/os/SharedMemory.java
+++ b/core/java/android/os/SharedMemory.java
@@ -25,8 +25,6 @@
import dalvik.system.VMRuntime;
-import libcore.io.IoUtils;
-
import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -65,7 +63,7 @@
mMemoryRegistration = new MemoryRegistration(mSize);
mCleaner = Cleaner.create(mFileDescriptor,
- new Closer(mFileDescriptor, mMemoryRegistration));
+ new Closer(mFileDescriptor.getInt$(), mMemoryRegistration));
}
/**
@@ -328,20 +326,21 @@
* Cleaner that closes the FD
*/
private static final class Closer implements Runnable {
- private FileDescriptor mFd;
+ private int mFd;
private MemoryRegistration mMemoryReference;
- private Closer(FileDescriptor fd, MemoryRegistration memoryReference) {
+ private Closer(int fd, MemoryRegistration memoryReference) {
mFd = fd;
- IoUtils.setFdOwner(mFd, this);
mMemoryReference = memoryReference;
}
@Override
public void run() {
- IoUtils.closeQuietly(mFd);
- mFd = null;
-
+ try {
+ FileDescriptor fd = new FileDescriptor();
+ fd.setInt$(mFd);
+ Os.close(fd);
+ } catch (ErrnoException e) { /* swallow error */ }
mMemoryReference.release();
mMemoryReference = null;
}
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index b01ffe5..c73a422 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -42,3 +42,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "haptics"
+ name: "vibration_xml_apis"
+ description: "Enabled System APIs for vibration effect XML parser and serializer"
+ bug: "347273158"
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/os/vibrator/persistence/ParsedVibration.java b/core/java/android/os/vibrator/persistence/ParsedVibration.java
index a16d21e..e5543ab 100644
--- a/core/java/android/os/vibrator/persistence/ParsedVibration.java
+++ b/core/java/android/os/vibrator/persistence/ParsedVibration.java
@@ -16,31 +16,35 @@
package android.os.vibrator.persistence;
+import static android.os.vibrator.Flags.FLAG_VIBRATION_XML_APIS;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorInfo;
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
- * The result of parsing a serialized vibration, which can be define by one or more
- * {@link VibrationEffect} and a resolution method.
+ * The result of parsing a serialized vibration.
+ *
+ * @see VibrationXmlParser
*
* @hide
*/
-@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
-public class ParsedVibration {
+@TestApi // This was used in CTS before the flag was introduced.
+@SystemApi
+@FlaggedApi(FLAG_VIBRATION_XML_APIS)
+public final class ParsedVibration {
private final List<VibrationEffect> mEffects;
/** @hide */
+ @TestApi
public ParsedVibration(@NonNull List<VibrationEffect> effects) {
mEffects = effects;
}
@@ -49,40 +53,28 @@
public ParsedVibration(@NonNull VibrationEffect effect) {
mEffects = List.of(effect);
}
+
/**
* Returns the first parsed vibration supported by {@code vibrator}, or {@code null} if none of
* the parsed vibrations are supported.
*
* @hide
*/
- @TestApi
+ @TestApi // This was used in CTS before the flag was introduced.
+ @SystemApi
+ @FlaggedApi(FLAG_VIBRATION_XML_APIS)
@Nullable
public VibrationEffect resolve(@NonNull Vibrator vibrator) {
return resolve(vibrator.getInfo());
}
/**
- * Returns the parsed vibrations for testing purposes.
- *
- * <p>Real callers should not use this method. Instead, they should resolve to a
- * {@link VibrationEffect} via {@link #resolve(Vibrator)}.
- *
- * @hide
- */
- @TestApi
- @VisibleForTesting
- @NonNull
- public List<VibrationEffect> getVibrationEffects() {
- return Collections.unmodifiableList(mEffects);
- }
-
- /**
* Same as {@link #resolve(Vibrator)}, but uses {@link VibratorInfo} instead for resolving.
*
* @hide
*/
@Nullable
- public final VibrationEffect resolve(@NonNull VibratorInfo info) {
+ public VibrationEffect resolve(@NonNull VibratorInfo info) {
for (int i = 0; i < mEffects.size(); i++) {
VibrationEffect effect = mEffects.get(i);
if (info.areVibrationFeaturesSupported(effect)) {
@@ -91,4 +83,21 @@
}
return null;
}
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ParsedVibration)) {
+ return false;
+ }
+ ParsedVibration other = (ParsedVibration) o;
+ return mEffects.equals(other.mEffects);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mEffects);
+ }
}
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
index 7202d9a..e2312e0 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
@@ -16,13 +16,15 @@
package android.os.vibrator.persistence;
+import static android.os.vibrator.Flags.FLAG_VIBRATION_XML_APIS;
+
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.VibrationEffect;
-import android.util.Slog;
import android.util.Xml;
import com.android.internal.vibrator.persistence.VibrationEffectXmlParser;
@@ -36,9 +38,12 @@
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -116,10 +121,10 @@
*
* @hide
*/
-@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+@TestApi // This was used in CTS before the flag was introduced.
+@SystemApi
+@FlaggedApi(FLAG_VIBRATION_XML_APIS)
public final class VibrationXmlParser {
- private static final String TAG = "VibrationXmlParser";
/**
* The MIME type for a xml holding a vibration.
@@ -168,55 +173,12 @@
}
/**
- * Parses XML content from given input stream into a {@link VibrationEffect}.
- *
- * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
- * serialization. As such, the root tag must be a "vibration" tag.
- *
- * <p>This parser fails silently and returns {@code null} if the content of the input stream
- * does not follow the schema or has unsupported values.
- *
- * @return the {@link VibrationEffect} if parsed successfully, {@code null} otherwise.
- * @throws IOException error reading from given {@link Reader}
- *
- * @hide
- */
- @TestApi
- @Nullable
- public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException {
- return parseVibrationEffect(reader, /* flags= */ 0);
- }
-
- /**
- * Parses XML content from given input stream into a {@link VibrationEffect}.
- *
- * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
- * serialization. As such, the root tag must be a "vibration" tag.
- *
- * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing
- * behavior.
- *
- * @hide
- */
- @Nullable
- public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags)
- throws IOException {
- try {
- return parseDocumentInternal(
- reader, flags, VibrationXmlParser::parseVibrationEffectInternal);
- } catch (XmlParserException | XmlPullParserException e) {
- Slog.w(TAG, "Error parsing vibration XML", e);
- return null;
- }
- }
-
- /**
* Parses XML content from given input stream into a {@link ParsedVibration}.
*
- * <p>It supports both the "vibration" and "vibration-select" root tags.
+ * <p>It supports both the "vibration-effect" and "vibration-select" root tags.
* <ul>
- * <li>If "vibration" is the root tag, the serialization provided through {@code reader}
- * should contain a valid serialization for a single vibration.
+ * <li>If "vibration-effect" is the root tag, the serialization provided should contain a
+ * valid serialization for a single vibration.
* <li>If "vibration-select" is the root tag, the serialization may contain one or more
* valid vibration serializations.
* </ul>
@@ -225,36 +187,95 @@
* vibration(s), and the caller can get a concrete {@link VibrationEffect} by resolving this
* result to a specific vibrator.
*
- * <p>This parser fails silently and returns {@code null} if the content of the input does not
- * follow the schema or has unsupported values.
+ * <p>This parser fails with an exception if the content of the input stream does not follow the
+ * schema or has unsupported values.
*
* @return a {@link ParsedVibration}
- * @throws IOException error reading from given {@link Reader}
+ * @throws IOException error reading from given {@link InputStream} or parsing the content.
*
* @hide
*/
- @TestApi
- @Nullable
+ @TestApi // Replacing test APIs used in CTS before the flagged system APIs was introduced.
+ @SystemApi
+ @FlaggedApi(FLAG_VIBRATION_XML_APIS)
+ @NonNull
+ public static ParsedVibration parse(@NonNull InputStream inputStream) throws IOException {
+ return parseDocument(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Parses XML content from given input stream into a single {@link VibrationEffect}.
+ *
+ * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
+ * serialization. As such, the root tag must be a "vibration-effect" tag.
+ *
+ * <p>This parser fails with an exception if the content of the input stream does not follow the
+ * schema or has unsupported values.
+ *
+ * @return the parsed {@link VibrationEffect}
+ * @throws IOException error reading from given {@link InputStream} or parsing the content.
+ *
+ * @hide
+ */
+ @TestApi // Replacing test APIs used in CTS before the flagged system APIs was introduced.
+ @SystemApi
+ @FlaggedApi(FLAG_VIBRATION_XML_APIS)
+ @NonNull
+ public static VibrationEffect parseVibrationEffect(@NonNull InputStream inputStream)
+ throws IOException {
+ return parseVibrationEffect(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Parses XML content from given {@link Reader} into a {@link VibrationEffect}.
+ *
+ * <p>Same as {@link #parseVibrationEffect(InputStream)}, but with a {@link Reader}.
+ *
+ * @hide
+ */
+ @NonNull
+ public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException {
+ return parseVibrationEffect(reader, /* flags= */ 0);
+ }
+
+ /**
+ * Parses XML content from given {@link Reader} into a {@link VibrationEffect}.
+ *
+ * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing
+ * behavior.
+ *
+ * @hide
+ */
+ @NonNull
+ public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags)
+ throws IOException {
+ return parseDocumentInternal(reader, flags,
+ VibrationXmlParser::parseVibrationEffectInternal);
+ }
+
+ /**
+ * Parses XML content from given {@link Reader} into a {@link ParsedVibration}.
+ *
+ * <p>Same as {@link #parse(InputStream)}, but with a {@link Reader}.
+ *
+ * @hide
+ */
+ @NonNull
public static ParsedVibration parseDocument(@NonNull Reader reader) throws IOException {
return parseDocument(reader, /* flags= */ 0);
}
/**
- * Parses XML content from given input stream into a {@link ParsedVibration}.
+ * Parses XML content from given {@link Reader} into a {@link ParsedVibration}.
*
* <p>Same as {@link #parseDocument(Reader)}, with extra flags to control the parsing behavior.
*
* @hide
*/
- @Nullable
+ @NonNull
public static ParsedVibration parseDocument(@NonNull Reader reader, @Flags int flags)
throws IOException {
- try {
- return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal);
- } catch (XmlParserException | XmlPullParserException e) {
- Slog.w(TAG, "Error parsing vibration/vibration-select XML", e);
- return null;
- }
+ return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal);
}
/**
@@ -262,7 +283,7 @@
* {@link ParsedVibration}.
*
* <p>Same as {@link #parseDocument(Reader, int)}, but, instead of parsing the full XML content,
- * it takes a parser that points to either a <vibration-effect> or a <vibration-select> start
+ * it takes a parser that points to either a "vibration-effect" or a "vibration-select" start
* tag. No other parser position, including start of document, is considered valid.
*
* <p>This method parses until an end "vibration-effect" or "vibration-select" tag (depending
@@ -270,37 +291,22 @@
* will point to the end tag.
*
* @throws IOException error parsing from given {@link TypedXmlPullParser}.
- * @throws VibrationXmlParserException if the XML tag cannot be parsed into a
- * {@link ParsedVibration}. The given {@code parser} might be pointing to a child XML tag
- * that caused the parser failure.
+ * The given {@code parser} might be pointing to a child XML tag that caused the parser
+ * failure.
*
* @hide
*/
@NonNull
public static ParsedVibration parseElement(@NonNull TypedXmlPullParser parser, @Flags int flags)
- throws IOException, VibrationXmlParserException {
+ throws IOException {
try {
return parseElementInternal(parser, flags);
} catch (XmlParserException e) {
- throw new VibrationXmlParserException("Error parsing vibration-select.", e);
+ throw new ParseFailedException(e);
}
}
- /**
- * Represents an error while parsing a vibration XML input.
- *
- * @hide
- */
- public static final class VibrationXmlParserException extends Exception {
- private VibrationXmlParserException(String message, Throwable cause) {
- super(message, cause);
- }
-
- private VibrationXmlParserException(String message) {
- super(message);
- }
- }
-
+ @NonNull
private static ParsedVibration parseElementInternal(
@NonNull TypedXmlPullParser parser, @Flags int flags)
throws IOException, XmlParserException {
@@ -313,11 +319,12 @@
case XmlConstants.TAG_VIBRATION_SELECT:
return parseVibrationSelectInternal(parser, flags);
default:
- throw new XmlParserException(
- "Unexpected tag name when parsing element: " + tagName);
+ throw new ParseFailedException(
+ "Unexpected tag " + tagName + " when parsing a vibration");
}
}
+ @NonNull
private static ParsedVibration parseVibrationSelectInternal(
@NonNull TypedXmlPullParser parser, @Flags int flags)
throws IOException, XmlParserException {
@@ -332,7 +339,7 @@
return new ParsedVibration(effects);
}
- /** Parses a single XML element for "vibration" tag into a {@link VibrationEffect}. */
+ @NonNull
private static VibrationEffect parseVibrationEffectInternal(
@NonNull TypedXmlPullParser parser, @Flags int flags)
throws IOException, XmlParserException {
@@ -347,32 +354,60 @@
* This method parses a whole XML document (provided through a {@link Reader}). The root tag is
* parsed as per a provided {@link ElementParser}.
*/
+ @NonNull
private static <T> T parseDocumentInternal(
@NonNull Reader reader, @Flags int flags, ElementParser<T> parseLogic)
- throws IOException, XmlParserException, XmlPullParserException {
- TypedXmlPullParser parser = Xml.newFastPullParser();
- parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
- parser.setInput(reader);
+ throws IOException {
+ try {
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ parser.setInput(reader);
- // Ensure XML starts with a document start tag.
- XmlReader.readDocumentStart(parser);
+ // Ensure XML starts with a document start tag.
+ XmlReader.readDocumentStart(parser);
- // Parse root tag.
- T result = parseLogic.parse(parser, flags);
+ // Parse root tag.
+ T result = parseLogic.parse(parser, flags);
- // Ensure XML ends after root tag is consumed.
- XmlReader.readDocumentEndTag(parser);
+ // Ensure XML ends after root tag is consumed.
+ XmlReader.readDocumentEndTag(parser);
- return result;
+ return result;
+ } catch (XmlPullParserException e) {
+ throw new ParseFailedException("Error initializing XMLPullParser", e);
+ } catch (XmlParserException e) {
+ throw new ParseFailedException(e);
+ }
}
/** Encapsulate a logic to parse an XML element from an open parser. */
private interface ElementParser<T> {
/** Parses a single XML element starting from the current position of the {@code parser}. */
+ @NonNull
T parse(@NonNull TypedXmlPullParser parser, @Flags int flags)
throws IOException, XmlParserException;
}
+ /**
+ * Represents an error while parsing a vibration XML input.
+ *
+ * @hide
+ */
+ @TestApi
+ public static final class ParseFailedException extends IOException {
+ private ParseFailedException(String message) {
+ super(message);
+ }
+
+ private ParseFailedException(XmlParserException parserException) {
+ this(parserException.getMessage(), parserException);
+ }
+
+ private ParseFailedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
private VibrationXmlParser() {
}
}
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index 2065d5d..a26c6f4 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -18,9 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
-import android.annotation.SuppressLint;
import android.annotation.TestApi;
-import android.os.CombinedVibration;
import android.os.VibrationEffect;
import android.util.Xml;
@@ -37,14 +35,13 @@
import java.lang.annotation.RetentionPolicy;
/**
- * Serializes {@link CombinedVibration} and {@link VibrationEffect} instances to XML.
+ * Serializes {@link VibrationEffect} instances to XML.
*
* <p>This uses the same schema expected by the {@link VibrationXmlParser}.
*
* @hide
*/
@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
public final class VibrationXmlSerializer {
/**
@@ -80,20 +77,19 @@
"http://xmlpull.org/v1/doc/features.html#indent-output";
/**
- * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer}.
+ * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer} using
+ * UTF-8 encoding.
*
- * <p>This method will only write into the {@link Writer} if the effect can successfully
- * be represented by the XML serialization. It will throw an exception otherwise.
+ * <p>This method will only write to the stream if the effect can successfully be represented by
+ * the XML serialization. It will throw an exception otherwise.
*
- * @throws SerializationFailedException serialization of input effect failed, no data was
- * written into given {@link Writer}.
- * @throws IOException error writing to given {@link Writer}.
+ * @throws IOException serialization of input effect failed or error writing to output stream.
*
* @hide
*/
@TestApi
public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer)
- throws SerializationFailedException, IOException {
+ throws IOException {
serialize(effect, writer, /* flags= */ 0);
}
@@ -106,7 +102,7 @@
* @hide
*/
public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer,
- @Flags int flags) throws SerializationFailedException, IOException {
+ @Flags int flags) throws IOException {
// Serialize effect first to fail early.
XmlSerializedVibration<VibrationEffect> serializedVibration =
toSerializedVibration(effect, flags);
@@ -138,17 +134,16 @@
}
/**
- * Exception thrown when a {@link VibrationEffect} instance serialization fails.
+ * Exception thrown when a {@link VibrationEffect} serialization fails.
*
* <p>The serialization can fail if a given vibration cannot be represented using the public
- * format, or if it uses hidden APIs that are not supported for serialization (e.g.
- * {@link VibrationEffect.WaveformBuilder}).
+ * format, or if it uses a non-public representation that is not supported for serialization.
*
* @hide
*/
@TestApi
- public static final class SerializationFailedException extends RuntimeException {
- SerializationFailedException(VibrationEffect effect, Throwable cause) {
+ public static final class SerializationFailedException extends IOException {
+ private SerializationFailedException(VibrationEffect effect, Throwable cause) {
super("Serialization failed for vibration effect " + effect, cause);
}
}
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d6425c3..ed20207 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -32,3 +32,14 @@
description: "Provides additional callbacks with information about user actions in ChooserResult"
bug: "263474465"
}
+
+flag {
+ name: "fix_resolver_memory_leak"
+ is_exported: true
+ namespace: "intentresolver"
+ description: "ResolverActivity memory leak (through the AppPredictor callback) fix"
+ bug: "346671041"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/util/SequenceUtils.java b/core/java/android/util/SequenceUtils.java
new file mode 100644
index 0000000..f833ce3
--- /dev/null
+++ b/core/java/android/util/SequenceUtils.java
@@ -0,0 +1,63 @@
+/*
+ * 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.util;
+
+/**
+ * Utilities to manage an info change seq id to ensure the update is in sync between client and
+ * system server. This should be used for info that can be updated though multiple IPC channel.
+ *
+ * To use it:
+ * 1. The system server should store the current seq as the source of truth, with initializing to
+ * {@link #getInitSeq}.
+ * 2. Whenever a newer info needs to be sent to the client side, the system server should first
+ * update its seq with {@link #getNextSeq}, then send the new info with the new seq to the client.
+ * 3. On the client side, when receiving a new info, it should only consume it if it is newer than
+ * the last received info seq by checking {@link #isIncomingSeqNewer}.
+ *
+ * @hide
+ */
+public final class SequenceUtils {
+
+ private SequenceUtils() {
+ }
+
+ /**
+ * Returns {@code true} if the incomingSeq is newer than the curSeq.
+ */
+ public static boolean isIncomingSeqNewer(int curSeq, int incomingSeq) {
+ // Convert to long for comparison.
+ final long diff = (long) incomingSeq - curSeq;
+ // If there has been a sufficiently large jump, assume the sequence has wrapped around.
+ // For example, when the last seq is MAX_VALUE, the incoming seq will be MIN_VALUE + 1.
+ // diff = MIN_VALUE + 1 - MAX_VALUE. It is smaller than 0, but should be treated as newer.
+ return diff > 0 || diff < Integer.MIN_VALUE;
+ }
+
+ /** Returns the initial seq. */
+ public static int getInitSeq() {
+ return Integer.MIN_VALUE;
+ }
+
+ /** Returns the next seq. */
+ public static int getNextSeq(int seq) {
+ return seq == Integer.MAX_VALUE
+ // Skip the initial seq, so that when the app process is relaunched, the incoming
+ // seq from the server is always treated as newer.
+ ? getInitSeq() + 1
+ : ++seq;
+ }
+}
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 487214c..2efa647 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -18,6 +18,7 @@
import static android.graphics.PointProto.X;
import static android.graphics.PointProto.Y;
+import static android.util.SequenceUtils.getInitSeq;
import static android.view.InsetsSourceControlProto.LEASH;
import static android.view.InsetsSourceControlProto.POSITION;
import static android.view.InsetsSourceControlProto.TYPE_NUMBER;
@@ -266,6 +267,9 @@
private @Nullable InsetsSourceControl[] mControls;
+ /** To make sure the info update between client and system server is in order. */
+ private int mSeq = getInitSeq();
+
public Array() {
}
@@ -280,9 +284,18 @@
readFromParcel(in);
}
+ public int getSeq() {
+ return mSeq;
+ }
+
+ public void setSeq(int seq) {
+ mSeq = seq;
+ }
+
/** Updates the current Array to the given Array. */
public void setTo(@NonNull Array other, boolean copyControls) {
set(other.mControls, copyControls);
+ mSeq = other.mSeq;
}
/** Updates the current controls to the given controls. */
@@ -336,11 +349,13 @@
public void readFromParcel(Parcel in) {
mControls = in.createTypedArray(InsetsSourceControl.CREATOR);
+ mSeq = in.readInt();
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeTypedArray(mControls, flags);
+ out.writeInt(mSeq);
}
public static final @NonNull Creator<Array> CREATOR = new Creator<>() {
@@ -362,6 +377,7 @@
return false;
}
final InsetsSourceControl.Array other = (InsetsSourceControl.Array) o;
+ // mSeq is for internal bookkeeping only.
return Arrays.equals(mControls, other.mControls);
}
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 21eec67..bbd9acf 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -17,6 +17,7 @@
package android.view;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.util.SequenceUtils.getInitSeq;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER;
import static android.view.InsetsStateProto.DISPLAY_CUTOUT;
@@ -95,6 +96,9 @@
/** The display shape */
private DisplayShape mDisplayShape = DisplayShape.NONE;
+ /** To make sure the info update between client and system server is in order. */
+ private int mSeq = getInitSeq();
+
public InsetsState() {
mSources = new SparseArray<>();
}
@@ -586,6 +590,14 @@
}
}
+ public int getSeq() {
+ return mSeq;
+ }
+
+ public void setSeq(int seq) {
+ mSeq = seq;
+ }
+
public void set(InsetsState other) {
set(other, false /* copySources */);
}
@@ -597,6 +609,7 @@
mRoundedCornerFrame.set(other.mRoundedCornerFrame);
mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
mDisplayShape = other.getDisplayShape();
+ mSeq = other.mSeq;
mSources.clear();
for (int i = 0, size = other.mSources.size(); i < size; i++) {
final InsetsSource otherSource = other.mSources.valueAt(i);
@@ -620,6 +633,7 @@
mRoundedCornerFrame.set(other.mRoundedCornerFrame);
mPrivacyIndicatorBounds = other.getPrivacyIndicatorBounds();
mDisplayShape = other.getDisplayShape();
+ mSeq = other.mSeq;
if (types == 0) {
return;
}
@@ -705,6 +719,7 @@
|| !mRoundedCornerFrame.equals(state.mRoundedCornerFrame)
|| !mPrivacyIndicatorBounds.equals(state.mPrivacyIndicatorBounds)
|| !mDisplayShape.equals(state.mDisplayShape)) {
+ // mSeq is for internal bookkeeping only.
return false;
}
@@ -778,6 +793,7 @@
mRoundedCornerFrame.writeToParcel(dest, flags);
dest.writeTypedObject(mPrivacyIndicatorBounds, flags);
dest.writeTypedObject(mDisplayShape, flags);
+ dest.writeInt(mSeq);
final int size = mSources.size();
dest.writeInt(size);
for (int i = 0; i < size; i++) {
@@ -803,6 +819,7 @@
mRoundedCornerFrame.readFromParcel(in);
mPrivacyIndicatorBounds = in.readTypedObject(PrivacyIndicatorBounds.CREATOR);
mDisplayShape = in.readTypedObject(DisplayShape.CREATOR);
+ mSeq = in.readInt();
final int size = in.readInt();
final SparseArray<InsetsSource> sources;
if (mSources == null) {
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 0bdb4ad..f653524 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -2812,6 +2812,10 @@
private final ArrayMap<SurfaceControl, Point> mResizedSurfaces = new ArrayMap<>();
private final ArrayMap<SurfaceControl, SurfaceControl> mReparentedSurfaces =
new ArrayMap<>();
+ // Only non-null if the SurfaceControlRegistry is enabled. This list tracks the set of calls
+ // made through this transaction object, and is dumped (and cleared) when the transaction is
+ // later applied.
+ ArrayList<String> mCalls;
Runnable mFreeNativeResources;
private static final float[] INVALID_COLOR = {-1, -1, -1};
@@ -2837,13 +2841,28 @@
private Transaction(long nativeObject) {
mNativeObject = nativeObject;
mFreeNativeResources = sRegistry.registerNativeAllocation(this, mNativeObject);
- if (!SurfaceControlRegistry.sCallStackDebuggingInitialized) {
- SurfaceControlRegistry.initializeCallStackDebugging();
- }
+ setUpForSurfaceControlRegistry();
}
private Transaction(Parcel in) {
readFromParcel(in);
+ setUpForSurfaceControlRegistry();
+ }
+
+ /**
+ * Sets up this transaction for the SurfaceControlRegistry.
+ */
+ private void setUpForSurfaceControlRegistry() {
+ if (!SurfaceControlRegistry.sCallStackDebuggingInitialized) {
+ SurfaceControlRegistry.initializeCallStackDebugging();
+ }
+ mCalls = SurfaceControlRegistry.sLogAllTxCallsOnApply
+ ? new ArrayList<>()
+ : null;
+ if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+ SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+ "ctor", this, null, null);
+ }
}
/**
@@ -2893,6 +2912,9 @@
if (mNativeObject != 0) {
nativeClearTransaction(mNativeObject);
}
+ if (mCalls != null) {
+ mCalls.clear();
+ }
}
/**
@@ -2904,6 +2926,9 @@
mReparentedSurfaces.clear();
mFreeNativeResources.run();
mNativeObject = 0;
+ if (mCalls != null) {
+ mCalls.clear();
+ }
}
/**
@@ -2921,7 +2946,10 @@
if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
- "apply", this, null, null);
+ SurfaceControlRegistry.APPLY, this, null, null);
+ }
+ if (mCalls != null) {
+ mCalls.clear();
}
}
@@ -4421,6 +4449,14 @@
if (this == other) {
return this;
}
+ if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+ SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+ "merge", this, null, "otherTx=" + other.getId());
+ if (mCalls != null) {
+ mCalls.addAll(other.mCalls);
+ other.mCalls.clear();
+ }
+ }
mResizedSurfaces.putAll(other.mResizedSurfaces);
other.mResizedSurfaces.clear();
mReparentedSurfaces.putAll(other.mReparentedSurfaces);
@@ -4472,6 +4508,10 @@
Log.w(TAG, "addTransactionCompletedListener was called but flag is disabled");
return this;
}
+ if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+ SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+ "setFrameTimeline", this, null, "vsyncId=" + vsyncId);
+ }
nativeSetFrameTimelineVsync(mNativeObject, vsyncId);
return this;
}
@@ -4479,6 +4519,11 @@
/** @hide */
@NonNull
public Transaction setFrameTimelineVsync(long frameTimelineVsyncId) {
+ if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
+ SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
+ "setFrameTimelineVsync", this, null, "frameTimelineVsyncId="
+ + frameTimelineVsyncId);
+ }
nativeSetFrameTimelineVsync(mNativeObject, frameTimelineVsyncId);
return this;
}
diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java
index aa3654d..117b200 100644
--- a/core/java/android/view/SurfaceControlRegistry.java
+++ b/core/java/android/view/SurfaceControlRegistry.java
@@ -44,6 +44,8 @@
*/
public class SurfaceControlRegistry {
private static final String TAG = "SurfaceControlRegistry";
+ // Special constant for identifying the Transaction#apply() calls
+ static final String APPLY = "apply";
/**
* An interface for processing the registered SurfaceControls when the threshold is exceeded.
@@ -134,6 +136,10 @@
// sCallStackDebuggingEnabled is true. Can be combined with the match name.
private static String sCallStackDebuggingMatchCall;
+ // When set, all calls on a SurfaceControl.Transaction will be stored and logged when the
+ // transaction is applied.
+ static boolean sLogAllTxCallsOnApply;
+
// Mapping of the active SurfaceControls to the elapsed time when they were registered
@GuardedBy("sLock")
private final WeakHashMap<SurfaceControl, Long> mSurfaceControls;
@@ -185,6 +191,7 @@
public void setCallStackDebuggingParams(String matchName, String matchCall) {
sCallStackDebuggingMatchName = matchName.toLowerCase();
sCallStackDebuggingMatchCall = matchCall.toLowerCase();
+ sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply");
}
/**
@@ -294,13 +301,15 @@
sCallStackDebuggingMatchName =
SystemProperties.get("persist.wm.debug.sc.tx.log_match_name", null)
.toLowerCase();
+ sLogAllTxCallsOnApply = sCallStackDebuggingMatchCall.contains("apply");
// Only enable stack debugging if any of the match filters are set
- sCallStackDebuggingEnabled = (!sCallStackDebuggingMatchCall.isEmpty()
- || !sCallStackDebuggingMatchName.isEmpty());
+ sCallStackDebuggingEnabled = !sCallStackDebuggingMatchCall.isEmpty()
+ || !sCallStackDebuggingMatchName.isEmpty();
if (sCallStackDebuggingEnabled) {
Log.d(TAG, "Enabling transaction call stack debugging:"
+ " matchCall=" + sCallStackDebuggingMatchCall
- + " matchName=" + sCallStackDebuggingMatchName);
+ + " matchName=" + sCallStackDebuggingMatchName
+ + " logCallsWithApply=" + sLogAllTxCallsOnApply);
}
}
@@ -319,15 +328,31 @@
if (!sCallStackDebuggingEnabled) {
return;
}
- if (!matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) {
- return;
- }
- final String txMsg = tx != null ? "tx=" + tx.getId() + " ": "";
- final String scMsg = sc != null ? " sc=" + sc.getName() + "": "";
+
+ final String txMsg = tx != null ? "tx=" + tx.getId() + " " : "";
+ final String scMsg = sc != null ? " sc=" + sc.getName() + "" : "";
final String msg = details != null
? call + " (" + txMsg + scMsg + ") " + details
: call + " (" + txMsg + scMsg + ")";
- Log.e(TAG, msg, new Throwable());
+ if (sLogAllTxCallsOnApply && tx != null) {
+ if (call == APPLY) {
+ // Log the apply and dump the calls on that transaction
+ Log.e(TAG, msg, new Throwable());
+ for (int i = 0; i < tx.mCalls.size(); i++) {
+ Log.d(TAG, " " + tx.mCalls.get(i));
+ }
+ } else if (matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) {
+ // Otherwise log this call to the transaction if it matches the tracked calls
+ Log.e(TAG, msg, new Throwable());
+ tx.mCalls.add(msg);
+ }
+ } else {
+ // Log this call if it matches the tracked calls
+ if (!matchesForCallStackDebugging(sc != null ? sc.getName() : null, call)) {
+ return;
+ }
+ Log.e(TAG, msg, new Throwable());
+ }
}
/**
@@ -388,6 +413,7 @@
pw.println("sCallStackDebuggingEnabled=" + sCallStackDebuggingEnabled);
pw.println("sCallStackDebuggingMatchName=" + sCallStackDebuggingMatchName);
pw.println("sCallStackDebuggingMatchCall=" + sCallStackDebuggingMatchCall);
+ pw.println("sLogAllTxCallsOnApply=" + sLogAllTxCallsOnApply);
}
}
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 3df72e8..8c3390c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30607,7 +30607,8 @@
* {@link #setPointerIcon(PointerIcon)} for mouse devices. Subclasses may override this to
* customize the icon for the given pointer.
*
- * For example, the pointer icon for a stylus pointer can be resolved in the following way:
+ * For example, to always show the PointerIcon.TYPE_HANDWRITING icon for a stylus pointer,
+ * the event can be resolved in the following way:
* <code><pre>
* @Override
* public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
@@ -30617,7 +30618,7 @@
* && (toolType == MotionEvent.TOOL_TYPE_STYLUS
* || toolType == MotionEvent.TOOL_TYPE_ERASER)) {
* // Show this pointer icon only if this pointer is a stylus.
- * return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_WAIT);
+ * return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HANDWRITING);
* }
* // Use the default logic for determining the pointer icon for other non-stylus pointers,
* // like for the mouse cursor.
@@ -33898,8 +33899,19 @@
protected int calculateFrameRateCategory() {
int category;
switch (getViewRootImpl().intermittentUpdateState()) {
- case ViewRootImpl.INTERMITTENT_STATE_INTERMITTENT -> category =
- FRAME_RATE_CATEGORY_NORMAL | FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
+ case ViewRootImpl.INTERMITTENT_STATE_INTERMITTENT -> {
+ if (!sToolkitFrameRateBySizeReadOnlyFlagValue) {
+ category = FRAME_RATE_CATEGORY_NORMAL;
+ } else {
+ // The size based frame rate category can only be LOW or NORMAL. If the size
+ // based frame rate category is LOW, we shouldn't vote for NORMAL for
+ // intermittent.
+ category = Math.min(
+ mSizeBasedFrameRateCategoryAndReason & ~FRAME_RATE_CATEGORY_REASON_MASK,
+ FRAME_RATE_CATEGORY_NORMAL);
+ }
+ category |= FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
+ }
case ViewRootImpl.INTERMITTENT_STATE_NOT_INTERMITTENT ->
category = mSizeBasedFrameRateCategoryAndReason;
default -> category = mLastFrameRateCategory;
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index d5398e6..781a901 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -16,6 +16,8 @@
package android.window;
+import static android.util.SequenceUtils.getInitSeq;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
@@ -53,6 +55,9 @@
public float compatScale = 1f;
+ /** To make sure the info update between client and system server is in order. */
+ public int seq = getInitSeq();
+
public ClientWindowFrames() {
}
@@ -74,6 +79,7 @@
}
isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout;
compatScale = other.compatScale;
+ seq = other.seq;
}
/** Needed for AIDL out parameters. */
@@ -84,6 +90,7 @@
attachedFrame = in.readTypedObject(Rect.CREATOR);
isParentFrameClippedByDisplayCutout = in.readBoolean();
compatScale = in.readFloat();
+ seq = in.readInt();
}
@Override
@@ -94,6 +101,7 @@
dest.writeTypedObject(attachedFrame, flags);
dest.writeBoolean(isParentFrameClippedByDisplayCutout);
dest.writeFloat(compatScale);
+ dest.writeInt(seq);
}
@Override
@@ -116,6 +124,7 @@
return false;
}
final ClientWindowFrames other = (ClientWindowFrames) o;
+ // seq is for internal bookkeeping only.
return frame.equals(other.frame)
&& displayFrame.equals(other.displayFrame)
&& parentFrame.equals(other.parentFrame)
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index b714682..985dc10 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -192,4 +192,15 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
+
+flag {
+ name: "ensure_wallpaper_in_transitions"
+ namespace: "windowing_frontend"
+ description: "Ensure that wallpaper window tokens are always present/available for collection in transitions"
+ bug: "347593088"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
index a7aef92..6b0ca9f 100644
--- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
+++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
@@ -181,6 +181,27 @@
}
/**
+ * Converts {@link Settings.Secure} key to {@link UserShortcutType}.
+ *
+ * @param key The shortcut key in Settings.
+ * @return The mapped type
+ */
+ @UserShortcutType
+ public static int convertToType(String key) {
+ return switch (key) {
+ case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> UserShortcutType.SOFTWARE;
+ case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> UserShortcutType.QUICK_SETTINGS;
+ case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> UserShortcutType.HARDWARE;
+ case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED ->
+ UserShortcutType.TRIPLETAP;
+ case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED ->
+ UserShortcutType.TWOFINGER_DOUBLETAP;
+ default -> throw new IllegalArgumentException(
+ "Unsupported user shortcut key: " + key);
+ };
+ }
+
+ /**
* Updates an accessibility state if the accessibility service is a Always-On a11y service,
* a.k.a. AccessibilityServices that has FLAG_REQUEST_ACCESSIBILITY_BUTTON
* <p>
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 920981e..a194535 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1209,9 +1209,19 @@
if (!isChangingConfigurations() && mPickOptionRequest != null) {
mPickOptionRequest.cancel();
}
- if (mMultiProfilePagerAdapter != null
- && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
- mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+ if (mMultiProfilePagerAdapter != null) {
+ ResolverListAdapter activeAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ if (activeAdapter != null) {
+ activeAdapter.onDestroy();
+ }
+ if (android.service.chooser.Flags.fixResolverMemoryLeak()) {
+ ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ if (inactiveAdapter != null) {
+ inactiveAdapter.onDestroy();
+ }
+ }
}
}
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 61eaa52..911bb19 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -111,6 +111,7 @@
"libminikin",
"libz",
"server_configurable_flags",
+ "libaconfig_storage_read_api_cc",
"android.database.sqlite-aconfig-cc",
"android.media.audiopolicy-aconfig-cc",
],
@@ -127,6 +128,7 @@
],
defaults: [
+ "aconfig_lib_cc_shared_link.defaults",
"latest_android_media_audio_common_types_cpp_target_shared",
],
@@ -365,6 +367,7 @@
"libdl_android",
"libtimeinstate",
"server_configurable_flags",
+ "libaconfig_storage_read_api_cc",
"libimage_io",
"libultrahdr",
"libperfetto_c",
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 7a166da..9d1e86b 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3833,6 +3833,11 @@
<!-- Message of notification shown when Test Harness Mode is enabled. [CHAR LIMIT=NONE] -->
<string name="test_harness_mode_notification_message">Perform a factory reset to disable Test Harness Mode.</string>
+ <!-- Title of notification shown when device is in the wrong Headless System User Mode configuration. [CHAR LIMIT=NONE] -->
+ <string name="wrong_hsum_configuration_notification_title">Wrong HSUM build configuration</string>
+ <!-- Message of notification shown when device is in the wrong Headless System User Mode configuration. [CHAR LIMIT=NONE] -->
+ <string name="wrong_hsum_configuration_notification_message">The Headless System User Mode state of this device differs from its build configuration. Please factory reset the device.</string>
+
<!-- Title of notification shown when serial console is enabled. [CHAR LIMIT=NONE] -->
<string name="console_running_notification_title">Serial console enabled</string>
<!-- Message of notification shown when serial console is enabled. [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0f54d89..8823894 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2153,6 +2153,8 @@
<java-symbol type="string" name="adbwifi_active_notification_title" />
<java-symbol type="string" name="test_harness_mode_notification_title" />
<java-symbol type="string" name="test_harness_mode_notification_message" />
+ <java-symbol type="string" name="wrong_hsum_configuration_notification_title" />
+ <java-symbol type="string" name="wrong_hsum_configuration_notification_message" />
<java-symbol type="string" name="console_running_notification_title" />
<java-symbol type="string" name="console_running_notification_message" />
<java-symbol type="string" name="mte_override_notification_title" />
diff --git a/core/tests/bugreports/Android.bp b/core/tests/bugreports/Android.bp
index 7c1ac48..15e07e5 100644
--- a/core/tests/bugreports/Android.bp
+++ b/core/tests/bugreports/Android.bp
@@ -30,6 +30,7 @@
"android.test.base",
],
static_libs: [
+ "android.tracing.flags-aconfig-java",
"androidx.test.rules",
"androidx.test.uiautomator_uiautomator",
"truth",
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 8072d69..7294d4c 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -71,6 +71,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -102,17 +103,11 @@
// associated with the bugreport).
private static final String INTENT_BUGREPORT_FINISHED =
"com.android.internal.intent.action.BUGREPORT_FINISHED";
- private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
- private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
- private static final Path[] UI_TRACES_PREDUMPED = {
+ private ArrayList<Path> mUiTracesPreDumped = new ArrayList<>(Arrays.asList(
Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"),
- Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
- Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
- Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
- Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
- Paths.get("/data/misc/wmtrace/wm_log.winscope"),
- };
+ Paths.get("/data/misc/wmtrace/wm_trace.winscope")
+ ));
private Handler mHandler;
private Executor mExecutor;
@@ -124,6 +119,17 @@
@Before
public void setup() throws Exception {
+ if (!android.tracing.Flags.perfettoIme()) {
+ mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"));
+ mUiTracesPreDumped.add(
+ Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"));
+ mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"));
+ }
+
+ if (!android.tracing.Flags.perfettoProtologTracing()) {
+ mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/wm_log.winscope"));
+ }
+
mHandler = createHandler();
mExecutor = (runnable) -> {
if (mHandler != null) {
@@ -206,7 +212,7 @@
mBrm.preDumpUiData();
waitTillDumpstateExitedOrTimeout();
- List<File> expectedPreDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
+ List<File> expectedPreDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
@@ -220,9 +226,9 @@
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertFdsAreClosed(mBugreportFd);
- assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+ assertThatBugreportContainsFiles(mUiTracesPreDumped);
- List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+ List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
}
@@ -235,9 +241,9 @@
// In some corner cases, data dumped as part of the full bugreport could be the same as the
// pre-dumped data and this test would fail. Hence, here we create fake/artificial
// pre-dumped data that we know it won't match with the full bugreport data.
- createFakeTraceFiles(UI_TRACES_PREDUMPED);
+ createFakeTraceFiles(mUiTracesPreDumped);
- List<File> preDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
+ List<File> preDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
BugreportCallbackImpl callback = new BugreportCallbackImpl();
mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
@@ -251,9 +257,9 @@
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertFdsAreClosed(mBugreportFd);
- assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+ assertThatBugreportContainsFiles(mUiTracesPreDumped);
- List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+ List<File> actualTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
}
@@ -270,7 +276,7 @@
// 1. Pre-dump data
// 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK)
// 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK)
- removeFilesIfNeeded(UI_TRACES_PREDUMPED);
+ removeFilesIfNeeded(mUiTracesPreDumped);
// Start bugreport with "use predump" flag. Because the pre-dumped data is not available
// the flag will be ignored and data will be dumped as in normal flow.
@@ -286,7 +292,7 @@
assertThat(mBugreportFile.length()).isGreaterThan(0L);
assertFdsAreClosed(mBugreportFd);
- assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+ assertThatBugreportContainsFiles(mUiTracesPreDumped);
}
@Test
@@ -555,7 +561,7 @@
);
}
- private void assertThatBugreportContainsFiles(Path[] paths)
+ private void assertThatBugreportContainsFiles(List<Path> paths)
throws IOException {
List<Path> entries = listZipArchiveEntries(mBugreportFile);
for (Path pathInDevice : paths) {
@@ -564,7 +570,7 @@
}
}
- private List<File> extractFilesFromBugreport(Path[] paths) throws Exception {
+ private List<File> extractFilesFromBugreport(List<Path> paths) throws Exception {
List<File> files = new ArrayList<File>();
for (Path pathInDevice : paths) {
Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
@@ -614,7 +620,7 @@
return extractedFile;
}
- private static void createFakeTraceFiles(Path[] paths) throws Exception {
+ private static void createFakeTraceFiles(List<Path> paths) throws Exception {
File src = createTempFile("fake", ".data");
Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
@@ -631,7 +637,7 @@
);
}
- private static List<File> copyFiles(Path[] paths) throws Exception {
+ private static List<File> copyFiles(List<Path> paths) throws Exception {
ArrayList<File> files = new ArrayList<File>();
for (Path src : paths) {
File dst = createTempFile(src.getFileName().toString(), ".copy");
@@ -643,7 +649,7 @@
return files;
}
- private static void removeFilesIfNeeded(Path[] paths) throws Exception {
+ private static void removeFilesIfNeeded(List<Path> paths) throws Exception {
for (Path path : paths) {
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
"rm -f " + path.toString()
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
index 3104f16..0b270d4 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java
@@ -20,8 +20,6 @@
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -104,8 +102,6 @@
@Before
public void setup() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
MockitoAnnotations.initMocks(this);
mDisplayManager = new DisplayManagerGlobal(mIDisplayManager);
mHandler = getInstrumentation().getContext().getMainThreadHandler();
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
index 5272416..79659ca 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java
@@ -16,22 +16,16 @@
package android.app.servertransaction;
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
-
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.ClientTransactionHandler;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -49,24 +43,8 @@
@Presubmit
public class ClientTransactionTests {
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
@Test
public void testPreExecute() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testPreExecuteInner();
- }
-
- @Test
- public void testPreExecute_bundleClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testPreExecuteInner();
- }
-
- private void testPreExecuteInner() {
final ClientTransactionItem callback1 = mock(ClientTransactionItem.class);
final ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class);
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
index 935bc75..73b7447 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java
@@ -25,9 +25,6 @@
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED;
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -54,14 +51,12 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArrayMap;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
@@ -88,9 +83,6 @@
@Presubmit
public class TransactionExecutorTests {
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
@Mock
private ClientTransactionHandler mTransactionHandler;
@Mock
@@ -248,19 +240,6 @@
@Test
public void testTransactionResolution() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testTransactionResolutionInner();
- }
-
- @Test
- public void testTransactionResolution_bundleClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testTransactionResolutionInner();
- }
-
- private void testTransactionResolutionInner() {
ClientTransactionItem callback1 = mock(ClientTransactionItem.class);
when(callback1.getPostExecutionState()).thenReturn(UNDEFINED);
ClientTransactionItem callback2 = mock(ClientTransactionItem.class);
@@ -284,19 +263,6 @@
@Test
public void testDoNotLaunchDestroyedActivity() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testDoNotLaunchDestroyedActivityInner();
- }
-
- @Test
- public void testDoNotLaunchDestroyedActivity_bundleClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testDoNotLaunchDestroyedActivityInner();
- }
-
- private void testDoNotLaunchDestroyedActivityInner() {
final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>();
when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed);
// Assume launch transaction is still in queue, so there is no client record.
@@ -329,19 +295,6 @@
@Test
public void testActivityResultRequiredStateResolution() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testActivityResultRequiredStateResolutionInner();
- }
-
- @Test
- public void testActivityResultRequiredStateResolution_bundleClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testActivityResultRequiredStateResolutionInner();
- }
-
- private void testActivityResultRequiredStateResolutionInner() {
when(mTransactionHandler.getActivity(any())).thenReturn(mock(Activity.class));
PostExecItem postExecItem = new PostExecItem(ON_RESUME);
@@ -495,19 +448,6 @@
@Test(expected = IllegalArgumentException.class)
public void testActivityItemNullRecordThrowsException() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testActivityItemNullRecordThrowsExceptionInner();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testActivityItemNullRecordThrowsException_bundleClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testActivityItemNullRecordThrowsExceptionInner();
- }
-
- private void testActivityItemNullRecordThrowsExceptionInner() {
final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
final IBinder token = mock(IBinder.class);
@@ -520,19 +460,6 @@
@Test
public void testActivityItemExecute() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testActivityItemExecuteInner();
- }
-
- @Test
- public void testActivityItemExecute_bundleClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- testActivityItemExecuteInner();
- }
-
- private void testActivityItemExecuteInner() {
final ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class);
when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED);
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index d451fe5..a4d7661 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -20,9 +20,6 @@
import static android.app.servertransaction.TestUtils.mergedConfig;
import static android.app.servertransaction.TestUtils.referrerIntentList;
import static android.app.servertransaction.TestUtils.resultInfoList;
-import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
-
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
import static org.junit.Assert.assertEquals;
@@ -40,14 +37,12 @@
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.window.ActivityWindowInfo;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -67,9 +62,6 @@
@Presubmit
public class TransactionParcelTests {
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
-
private Parcel mParcel;
private IBinder mActivityToken;
@@ -296,8 +288,6 @@
@Test
public void testClientTransaction() {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
// Write to parcel
NewIntentItem callback1 = NewIntentItem.obtain(mActivityToken, new ArrayList<>(), true);
ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
@@ -320,49 +310,6 @@
assertEquals(mActivityToken, result.getActivityToken());
}
- @Test
- public void testClientTransactionCallbacksOnly() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- // Write to parcel
- NewIntentItem callback1 = NewIntentItem.obtain(mActivityToken, new ArrayList<>(), true);
- ActivityConfigurationChangeItem callback2 = ActivityConfigurationChangeItem.obtain(
- mActivityToken, config(), new ActivityWindowInfo());
-
- ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
- transaction.addTransactionItem(callback1);
- transaction.addTransactionItem(callback2);
-
- writeAndPrepareForReading(transaction);
-
- // Read from parcel and assert
- ClientTransaction result = ClientTransaction.CREATOR.createFromParcel(mParcel);
-
- assertEquals(transaction.hashCode(), result.hashCode());
- assertEquals(transaction, result);
- assertEquals(mActivityToken, result.getActivityToken());
- }
-
- @Test
- public void testClientTransactionLifecycleOnly() {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- // Write to parcel
- StopActivityItem lifecycleRequest = StopActivityItem.obtain(mActivityToken);
-
- ClientTransaction transaction = ClientTransaction.obtain(null /* client */);
- transaction.addTransactionItem(lifecycleRequest);
-
- writeAndPrepareForReading(transaction);
-
- // Read from parcel and assert
- ClientTransaction result = ClientTransaction.CREATOR.createFromParcel(mParcel);
-
- assertEquals(transaction.hashCode(), result.hashCode());
- assertEquals(transaction, result);
- assertEquals(mActivityToken, result.getActivityToken());
- }
-
/** Write to {@link #mParcel} and reset its position to prepare for reading from the start. */
private void writeAndPrepareForReading(Parcelable parcelable) {
parcelable.writeToParcel(mParcel, 0 /* flags */);
diff --git a/core/tests/coretests/src/android/content/ContentResolverTest.java b/core/tests/coretests/src/android/content/ContentResolverTest.java
index c8015d4..7b70b41 100644
--- a/core/tests/coretests/src/android/content/ContentResolverTest.java
+++ b/core/tests/coretests/src/android/content/ContentResolverTest.java
@@ -87,7 +87,7 @@
bitmap.compress(Bitmap.CompressFormat.PNG, 90, mImage.getOutputStream());
final AssetFileDescriptor afd = new AssetFileDescriptor(
- ParcelFileDescriptor.dup(mImage.getFileDescriptor()), 0, mSize, null);
+ new ParcelFileDescriptor(mImage.getFileDescriptor()), 0, mSize, null);
when(mProvider.openTypedAssetFile(any(), any(), any(), any(), any())).thenReturn(
afd);
}
diff --git a/core/tests/coretests/src/android/os/MessageQueueTest.java b/core/tests/coretests/src/android/os/MessageQueueTest.java
index 851e612..8cd6773 100644
--- a/core/tests/coretests/src/android/os/MessageQueueTest.java
+++ b/core/tests/coretests/src/android/os/MessageQueueTest.java
@@ -16,7 +16,6 @@
package android.os;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.MediumTest;
@@ -154,7 +153,6 @@
@Test
@MediumTest
- @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
public void testFieldIntegrity() throws Exception {
TestHandlerThread tester = new TestFieldIntegrityHandler() {
diff --git a/core/tests/coretests/src/android/util/SequenceUtilsTest.java b/core/tests/coretests/src/android/util/SequenceUtilsTest.java
new file mode 100644
index 0000000..020520d
--- /dev/null
+++ b/core/tests/coretests/src/android/util/SequenceUtilsTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.util;
+
+
+import static android.util.SequenceUtils.getInitSeq;
+import static android.util.SequenceUtils.getNextSeq;
+import static android.util.SequenceUtils.isIncomingSeqNewer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for subtypes of {@link SequenceUtils}.
+ *
+ * Build/Install/Run:
+ * atest FrameworksCoreTests:SequenceUtilsTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+@DisabledOnRavenwood(blockedBy = SequenceUtils.class)
+public class SequenceUtilsTest {
+
+ // This is needed to disable the test in Ravenwood test, because SequenceUtils hasn't opted in
+ // for Ravenwood, which is still in experiment.
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ public void testNextSeq() {
+ assertEquals(getInitSeq() + 1, getNextSeq(getInitSeq()));
+ assertEquals(getInitSeq() + 1, getNextSeq(Integer.MAX_VALUE));
+ }
+
+ @Test
+ public void testIsIncomingSeqNewer() {
+ assertTrue(isIncomingSeqNewer(getInitSeq() + 1, getInitSeq() + 10));
+ assertFalse(isIncomingSeqNewer(getInitSeq() + 10, getInitSeq() + 1));
+ assertTrue(isIncomingSeqNewer(-100, 100));
+ assertFalse(isIncomingSeqNewer(100, -100));
+ assertTrue(isIncomingSeqNewer(1, 2));
+ assertFalse(isIncomingSeqNewer(2, 1));
+
+ // Possible incoming seq are all newer than the initial seq.
+ assertTrue(isIncomingSeqNewer(getInitSeq(), getInitSeq() + 1));
+ assertTrue(isIncomingSeqNewer(getInitSeq(), -100));
+ assertTrue(isIncomingSeqNewer(getInitSeq(), 0));
+ assertTrue(isIncomingSeqNewer(getInitSeq(), 100));
+ assertTrue(isIncomingSeqNewer(getInitSeq(), Integer.MAX_VALUE));
+ assertTrue(isIncomingSeqNewer(getInitSeq(), getNextSeq(Integer.MAX_VALUE)));
+
+ // False for the same seq.
+ assertFalse(isIncomingSeqNewer(getInitSeq(), getInitSeq()));
+ assertFalse(isIncomingSeqNewer(100, 100));
+ assertFalse(isIncomingSeqNewer(Integer.MAX_VALUE, Integer.MAX_VALUE));
+
+ // True when there is a large jump (overflow).
+ assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getInitSeq() + 1));
+ assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getInitSeq() + 100));
+ assertTrue(isIncomingSeqNewer(Integer.MAX_VALUE, getNextSeq(Integer.MAX_VALUE)));
+ }
+}
diff --git a/core/tests/coretests/src/android/util/SparseSetArrayTest.java b/core/tests/coretests/src/android/util/SparseSetArrayTest.java
index 1c72185..a8dce70 100644
--- a/core/tests/coretests/src/android/util/SparseSetArrayTest.java
+++ b/core/tests/coretests/src/android/util/SparseSetArrayTest.java
@@ -17,7 +17,6 @@
import static com.google.common.truth.Truth.assertThat;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
@@ -37,7 +36,6 @@
public final RavenwoodRule mRavenwood = new RavenwoodRule();
@Test
- @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
public void testAddAll() {
final SparseSetArray<Integer> sparseSetArray = new SparseSetArray<>();
@@ -59,7 +57,6 @@
}
@Test
- @IgnoreUnderRavenwood(reason = "b/315036461")
public void testCopyConstructor() {
final SparseSetArray<Integer> sparseSetArray = new SparseSetArray<>();
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index 06cb0ee..b153700 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1250,6 +1250,81 @@
});
}
+ @Test
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_FUNCTION_ENABLING_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY})
+ public void votePreferredFrameRate_infrequentLayer_smallView_voteForLow() throws Throwable {
+ if (!ViewProperties.vrr_enabled().orElse(true)) {
+ return;
+ }
+ final long delay = 200L;
+
+ mView = new View(sContext);
+ WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
+ wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
+ wmlp.width = 1;
+ wmlp.height = 1;
+
+ // The view is a small view, and it should vote for category low only.
+ int expected = FRAME_RATE_CATEGORY_LOW;
+
+ sInstrumentation.runOnMainSync(() -> {
+ WindowManager wm = sContext.getSystemService(WindowManager.class);
+ wm.addView(mView, wmlp);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ mViewRootImpl = mView.getViewRootImpl();
+ waitForFrameRateCategoryToSettle(mView);
+
+ // In transition from frequent update to infrequent update
+ Thread.sleep(delay);
+ sInstrumentation.runOnMainSync(() -> {
+ mView.invalidate();
+ runAfterDraw(() -> assertEquals(expected,
+ mViewRootImpl.getLastPreferredFrameRateCategory()));
+ });
+ waitForAfterDraw();
+
+ // In transition from frequent update to infrequent update
+ Thread.sleep(delay);
+ sInstrumentation.runOnMainSync(() -> {
+ mView.invalidate();
+ runAfterDraw(() -> assertEquals(expected,
+ mViewRootImpl.getLastPreferredFrameRateCategory()));
+ });
+
+ // Infrequent update
+ Thread.sleep(delay);
+
+ // The view is small, the expected category is still low for intermittent.
+ int intermittentExpected = FRAME_RATE_CATEGORY_LOW;
+
+ sInstrumentation.runOnMainSync(() -> {
+ mView.invalidate();
+ runAfterDraw(() -> assertEquals(intermittentExpected,
+ mViewRootImpl.getLastPreferredFrameRateCategory()));
+ });
+ waitForAfterDraw();
+
+ // When the View vote, it's still considered as intermittent update state
+ sInstrumentation.runOnMainSync(() -> {
+ mView.invalidate();
+ runAfterDraw(() -> assertEquals(intermittentExpected,
+ mViewRootImpl.getLastPreferredFrameRateCategory()));
+ });
+ waitForAfterDraw();
+
+ // Becomes frequent update state
+ sInstrumentation.runOnMainSync(() -> {
+ mView.invalidate();
+ runAfterDraw(() -> assertEquals(expected,
+ mViewRootImpl.getLastPreferredFrameRateCategory()));
+ });
+ }
+
/**
* Test the IsFrameRatePowerSavingsBalanced values are properly set
*/
diff --git a/core/tests/utiltests/src/android/util/TimeUtilsTest.java b/core/tests/utiltests/src/android/util/TimeUtilsTest.java
index ac659e1..6c6feaf 100644
--- a/core/tests/utiltests/src/android/util/TimeUtilsTest.java
+++ b/core/tests/utiltests/src/android/util/TimeUtilsTest.java
@@ -18,17 +18,19 @@
import static org.junit.Assert.assertEquals;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.util.TimeZone;
import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
@@ -42,6 +44,22 @@
public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
+ private TimeZone mOrigTimezone;
+
+ @Before
+ public void setUp() {
+ mOrigTimezone = TimeZone.getDefault();
+
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+ }
+
+ @After
+ public void tearDown() {
+ if (mOrigTimezone != null) {
+ TimeZone.setDefault(mOrigTimezone);
+ }
+ }
+
@Test
public void testFormatTime() {
assertEquals("1672556400000 (now)",
@@ -85,32 +103,29 @@
}
@Test
- @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
public void testDumpTime() {
- assertEquals("2023-01-01 00:00:00.000", runWithPrintWriter((pw) -> {
+ assertEquals("2023-01-01 07:00:00.000", runWithPrintWriter((pw) -> {
TimeUtils.dumpTime(pw, 1672556400000L);
}));
- assertEquals("2023-01-01 00:00:00.000 (now)", runWithPrintWriter((pw) -> {
+ assertEquals("2023-01-01 07:00:00.000 (now)", runWithPrintWriter((pw) -> {
TimeUtils.dumpTimeWithDelta(pw, 1672556400000L, 1672556400000L);
}));
- assertEquals("2023-01-01 00:00:00.000 (-10ms)", runWithPrintWriter((pw) -> {
+ assertEquals("2023-01-01 07:00:00.000 (-10ms)", runWithPrintWriter((pw) -> {
TimeUtils.dumpTimeWithDelta(pw, 1672556400000L, 1672556400000L + 10);
}));
}
@Test
- @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
public void testFormatForLogging() {
assertEquals("unknown", TimeUtils.formatForLogging(0));
assertEquals("unknown", TimeUtils.formatForLogging(-1));
assertEquals("unknown", TimeUtils.formatForLogging(Long.MIN_VALUE));
- assertEquals("2023-01-01 00:00:00", TimeUtils.formatForLogging(1672556400000L));
+ assertEquals("2023-01-01 07:00:00", TimeUtils.formatForLogging(1672556400000L));
}
@Test
- @IgnoreUnderRavenwood(reason = "Flaky test, b/315872700")
public void testLogTimeOfDay() {
- assertEquals("01-01 00:00:00.000", TimeUtils.logTimeOfDay(1672556400000L));
+ assertEquals("01-01 07:00:00.000", TimeUtils.logTimeOfDay(1672556400000L));
}
public static String runWithPrintWriter(Consumer<PrintWriter> consumer) {
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
index 94298dc..83a8f8f 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
@@ -63,6 +63,34 @@
}
@Test
+ public void testEquals() {
+ assertThat(new ParsedVibration(List.of())).isEqualTo(new ParsedVibration(List.of()));
+ assertThat(new ParsedVibration(List.of())).isNotEqualTo(new ParsedVibration(mEffect1));
+ assertThat(new ParsedVibration(mEffect1)).isEqualTo(new ParsedVibration(mEffect1));
+ assertThat(new ParsedVibration(mEffect1)).isNotEqualTo(new ParsedVibration(mEffect2));
+ assertThat(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)))
+ .isEqualTo(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)));
+ assertThat(new ParsedVibration(List.of(mEffect1, mEffect2)))
+ .isNotEqualTo(new ParsedVibration(List.of(mEffect2, mEffect1)));
+ }
+
+ @Test
+ public void testHashCode() {
+ assertThat(new ParsedVibration(mEffect1).hashCode())
+ .isEqualTo(new ParsedVibration(mEffect1).hashCode());
+ assertThat(new ParsedVibration(mEffect1).hashCode())
+ .isNotEqualTo(new ParsedVibration(mEffect2).hashCode());
+ assertThat(new ParsedVibration(List.of()).hashCode())
+ .isEqualTo(new ParsedVibration(List.of()).hashCode());
+ assertThat(new ParsedVibration(List.of()).hashCode())
+ .isNotEqualTo(new ParsedVibration(mEffect1).hashCode());
+ assertThat(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)).hashCode())
+ .isEqualTo(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)).hashCode());
+ assertThat(new ParsedVibration(List.of(mEffect1, mEffect2)).hashCode())
+ .isNotEqualTo(new ParsedVibration(List.of(mEffect2, mEffect1)).hashCode());
+ }
+
+ @Test
public void testResolve_allUnsupportedVibrations() {
when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(false);
@@ -91,21 +119,6 @@
.isEqualTo(mEffect1);
}
- @Test
- public void testGetVibrationEffects() {
- ParsedVibration parsedVibration =
- new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3));
- assertThat(parsedVibration.getVibrationEffects())
- .containsExactly(mEffect1, mEffect2, mEffect3)
- .inOrder();
-
- parsedVibration = new ParsedVibration(List.of(mEffect1));
- assertThat(parsedVibration.getVibrationEffects()).containsExactly(mEffect1);
-
- parsedVibration = new ParsedVibration(List.of());
- assertThat(parsedVibration.getVibrationEffects()).isEmpty();
- }
-
private Subject assertThatResolution(
Vibrator vibrator, List<VibrationEffect> componentVibrations) {
return assertThat(new ParsedVibration(componentVibrations).resolve(vibrator));
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 7d8c53f..bf9a820 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -37,9 +37,9 @@
import org.junit.runners.JUnit4;
import org.xmlpull.v1.XmlPullParser;
-import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -74,18 +74,22 @@
.addPrimitive(PRIMITIVE_CLICK)
.addPrimitive(PRIMITIVE_TICK, 0.2497f)
.compose();
- String xml = "<vibration-effect>"
- + "<primitive-effect name=\"click\"/>"
- + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
- + "</vibration-effect>";
+ String xml = """
+ <vibration-effect>
+ <primitive-effect name="click"/>
+ <primitive-effect name="tick" scale="0.2497"/>
+ </vibration-effect>
+ """.trim();
VibrationEffect effect2 = VibrationEffect.startComposition()
.addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
.addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
.compose();
- String xml2 = "<vibration-effect>"
- + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
- + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
- + "</vibration-effect>";
+ String xml2 = """
+ <vibration-effect>
+ <primitive-effect name="low_tick" delayMs="356"/>
+ <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+ </vibration-effect>
+ """.trim();
TypedXmlPullParser parser = createXmlPullParser(xml);
assertParseElementSucceeds(parser, effect);
@@ -114,7 +118,12 @@
assertEndOfDocument(parser);
// Check when there is comment before the end tag.
- xml = "<vibration-effect><primitive-effect name=\"tick\"/><!-- hi --></vibration-effect>";
+ xml = """
+ <vibration-effect>
+ <primitive-effect name="tick"/>
+ <!-- hi -->
+ </vibration-effect>
+ """.trim();
parser = createXmlPullParser(xml);
assertParseElementSucceeds(
parser, VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK).compose());
@@ -128,18 +137,22 @@
.addPrimitive(PRIMITIVE_CLICK)
.addPrimitive(PRIMITIVE_TICK, 0.2497f)
.compose();
- String vibrationXml1 = "<vibration-effect>"
- + "<primitive-effect name=\"click\"/>"
- + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
- + "</vibration-effect>";
+ String vibrationXml1 = """
+ <vibration-effect>
+ <primitive-effect name="click"/>
+ <primitive-effect name="tick" scale="0.2497"/>
+ </vibration-effect>
+ """.trim();
VibrationEffect effect2 = VibrationEffect.startComposition()
.addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
.addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
.compose();
- String vibrationXml2 = "<vibration-effect>"
- + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
- + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
- + "</vibration-effect>";
+ String vibrationXml2 = """
+ <vibration-effect>
+ <primitive-effect name="low_tick" delayMs="356"/>
+ <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+ </vibration-effect>
+ """.trim();
String xml = "<vibration-select>" + vibrationXml1 + vibrationXml2 + "</vibration-select>";
TypedXmlPullParser parser = createXmlPullParser(xml);
@@ -183,8 +196,11 @@
@Test
public void testParseElement_withHiddenApis_onlySucceedsWithFlag() throws Exception {
// Check when the root tag is "vibration".
- String xml =
- "<vibration-effect><predefined-effect name=\"texture_tick\"/></vibration-effect>";
+ String xml = """
+ <vibration-effect>
+ <predefined-effect name="texture_tick"/>
+ </vibration-effect>
+ """.trim();
assertParseElementSucceeds(createXmlPullParser(xml),
VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
@@ -199,131 +215,186 @@
}
@Test
- public void testParseElement_badXml_throwsException() throws Exception {
+ public void testParseElement_badXml_throwsException() {
// No "vibration-select" tag.
- assertParseElementFails(
- "<vibration-effect>rand text<primitive-effect name=\"click\"/></vibration-effect>");
- assertParseElementFails("<bad-tag><primitive-effect name=\"click\"/></vibration-effect>");
- assertParseElementFails("<primitive-effect name=\"click\"/></vibration-effect>");
- assertParseElementFails("<vibration-effect><primitive-effect name=\"click\"/>");
+ assertParseElementFails("""
+ <vibration-effect>
+ rand text
+ <primitive-effect name="click"/>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <bad-tag>
+ <primitive-effect name="click"/>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <primitive-effect name="click"/>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <primitive-effect name="click"/>
+ """);
// Incomplete XML.
- assertParseElementFails("<vibration-select><primitive-effect name=\"click\"/>");
- assertParseElementFails("<vibration-select>"
- + "<vibration-effect>"
- + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
- + "</vibration-effect>");
+ assertParseElementFails("""
+ <vibration-select>
+ <primitive-effect name="click"/>
+ """);
+ assertParseElementFails("""
+ <vibration-select>
+ <vibration-effect>
+ <primitive-effect name="low_tick" delayMs="356"/>
+ </vibration-effect>
+ """);
// Bad vibration XML.
- assertParseElementFails("<vibration-select>"
- + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
- + "</vibration-effect>"
- + "</vibration-select>");
+ assertParseElementFails("""
+ <vibration-select>
+ <primitive-effect name="low_tick" delayMs="356"/>
+ </vibration-effect>
+ </vibration-select>
+ """);
// "vibration-select" tag should have no attributes.
- assertParseElementFails("<vibration-select bad_attr=\"123\">"
- + "<vibration-effect>"
- + "<predefined-effect name=\"tick\"/>"
- + "</vibration-effect>"
- + "</vibration-select>");
+ assertParseElementFails("""
+ <vibration-select bad_attr="123">
+ <vibration-effect>
+ <predefined-effect name="tick"/>
+ </vibration-effect>
+ </vibration-select>
+ """);
}
@Test
- public void testPrimitives_allSucceed() throws IOException {
+ public void testInvalidEffects_allFail() {
+ // Invalid root tag.
+ String xml = """
+ <vibration>
+ <predefined-effect name="click"/>
+ </vibration>
+ """;
+
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ // Invalid effect name.
+ xml = """
+ <vibration-effect>
+ <predefined-effect name="invalid"/>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ public void testVibrationSelectTag_onlyParseDocumentSucceeds() throws Exception {
+ VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+ String xml = """
+ <vibration-select>
+ <vibration-effect><predefined-effect name="click"/></vibration-effect>
+ </vibration-select>
+ """;
+
+ assertPublicApisParseDocumentSucceeds(xml, effect);
+ assertHiddenApisParseDocumentSucceeds(xml, effect);
+
+ assertPublicApisParseVibrationEffectFails(xml);
+ assertHiddenApisParseVibrationEffectFails(xml);
+ }
+
+ @Test
+ public void testPrimitives_allSucceed() throws Exception {
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(PRIMITIVE_CLICK)
.addPrimitive(PRIMITIVE_TICK, 0.2497f)
.addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
.addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
.compose();
- String xml = "<vibration-effect>"
- + "<primitive-effect name=\"click\"/>"
- + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
- + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
- + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
- + "</vibration-effect>";
+ String xml = """
+ <vibration-effect>
+ <primitive-effect name="click"/>
+ <primitive-effect name="tick" scale="0.2497"/>
+ <primitive-effect name="low_tick" delayMs="356"/>
+ <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+ </vibration-effect>
+ """;
assertPublicApisParserSucceeds(xml, effect);
assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
assertPublicApisRoundTrip(effect);
- assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+ assertHiddenApisParserSucceeds(xml, effect);
assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
assertHiddenApisRoundTrip(effect);
}
@Test
- public void testParseDocument_withVibrationSelectTag_withHiddenApis_onlySucceedsWithFlag()
- throws Exception {
- // Check when the root tag is "vibration-effect".
- String xml =
- "<vibration-effect><predefined-effect name=\"texture_tick\"/></vibration-effect>";
- assertParseDocumentSucceeds(xml,
- VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
- VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
- assertThat(parseDocument(xml, /* flags= */ 0)).isNull();
-
- // Check when the root tag is "vibration-select".
- xml = "<vibration-select>" + xml + "</vibration-select>";
- assertParseDocumentSucceeds(xml,
- VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
- VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
- assertThat(parseDocument(xml, /* flags= */ 0)).isNull();
- }
-
- @Test
- public void testWaveforms_allSucceed() throws IOException {
+ public void testWaveforms_allSucceed() throws Exception {
VibrationEffect effect = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
new int[]{254, 1, 255, 0}, /* repeat= */ 0);
- String xml = "<vibration-effect>"
- + "<waveform-effect><repeating>"
- + "<waveform-entry durationMs=\"123\" amplitude=\"254\"/>"
- + "<waveform-entry durationMs=\"456\" amplitude=\"1\"/>"
- + "<waveform-entry durationMs=\"789\" amplitude=\"255\"/>"
- + "<waveform-entry durationMs=\"0\" amplitude=\"0\"/>"
- + "</repeating></waveform-effect>"
- + "</vibration-effect>";
+ String xml = """
+ <vibration-effect>
+ <waveform-effect>
+ <repeating>
+ <waveform-entry durationMs="123" amplitude="254"/>
+ <waveform-entry durationMs="456" amplitude="1"/>
+ <waveform-entry durationMs="789" amplitude="255"/>
+ <waveform-entry durationMs="0" amplitude="0"/>
+ </repeating>
+ </waveform-effect>
+ </vibration-effect>
+ """;
assertPublicApisParserSucceeds(xml, effect);
assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
assertPublicApisRoundTrip(effect);
- assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+ assertHiddenApisParserSucceeds(xml, effect);
assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
assertHiddenApisRoundTrip(effect);
}
@Test
public void testPredefinedEffects_publicEffectsWithDefaultFallback_allSucceed()
- throws IOException {
+ throws Exception {
for (Map.Entry<String, Integer> entry : createPublicPredefinedEffectsMap().entrySet()) {
VibrationEffect effect = VibrationEffect.get(entry.getValue());
- String xml = String.format(
- "<vibration-effect><predefined-effect name=\"%s\"/></vibration-effect>",
+ String xml = String.format("""
+ <vibration-effect>
+ <predefined-effect name="%s"/>
+ </vibration-effect>
+ """,
entry.getKey());
assertPublicApisParserSucceeds(xml, effect);
assertPublicApisSerializerSucceeds(effect, entry.getKey());
assertPublicApisRoundTrip(effect);
- assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+ assertHiddenApisParserSucceeds(xml, effect);
assertHiddenApisSerializerSucceeds(effect, entry.getKey());
assertHiddenApisRoundTrip(effect);
}
}
@Test
- public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws IOException {
+ public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws Exception {
for (Map.Entry<String, Integer> entry : createHiddenPredefinedEffectsMap().entrySet()) {
VibrationEffect effect = VibrationEffect.get(entry.getValue());
- String xml = String.format(
- "<vibration-effect><predefined-effect name=\"%s\"/></vibration-effect>",
+ String xml = String.format("""
+ <vibration-effect>
+ <predefined-effect name="%s"/>
+ </vibration-effect>
+ """,
entry.getKey());
assertPublicApisParserFails(xml);
assertPublicApisSerializerFails(effect);
- assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+ assertHiddenApisParserSucceeds(xml, effect);
assertHiddenApisSerializerSucceeds(effect, entry.getKey());
assertHiddenApisRoundTrip(effect);
}
@@ -331,33 +402,119 @@
@Test
public void testPredefinedEffects_allEffectsWithNonDefaultFallback_onlySucceedsWithFlag()
- throws IOException {
+ throws Exception {
for (Map.Entry<String, Integer> entry : createAllPredefinedEffectsMap().entrySet()) {
boolean nonDefaultFallback = !PrebakedSegment.DEFAULT_SHOULD_FALLBACK;
VibrationEffect effect = VibrationEffect.get(entry.getValue(), nonDefaultFallback);
- String xml = String.format(
- "<vibration-effect><predefined-effect name=\"%s\" fallback=\"%s\"/>"
- + "</vibration-effect>",
+ String xml = String.format("""
+ <vibration-effect>
+ <predefined-effect name="%s" fallback="%s"/>
+ </vibration-effect>
+ """,
entry.getKey(), nonDefaultFallback);
assertPublicApisParserFails(xml);
assertPublicApisSerializerFails(effect);
- assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+ assertHiddenApisParserSucceeds(xml, effect);
assertHiddenApisSerializerSucceeds(effect, entry.getKey());
assertHiddenApisRoundTrip(effect);
}
}
- private void assertPublicApisParserFails(String xml) throws IOException {
- assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isNull();
+ private void assertPublicApisParserFails(String xml) {
+ assertThrows("Expected parseVibrationEffect to fail for " + xml,
+ VibrationXmlParser.ParseFailedException.class,
+ () -> parseVibrationEffect(xml, /* flags= */ 0));
+ assertThrows("Expected parseDocument to fail for " + xml,
+ VibrationXmlParser.ParseFailedException.class,
+ () -> parseDocument(xml, /* flags= */ 0));
+ }
+
+ private void assertPublicApisParseVibrationEffectFails(String xml) {
+ assertThrows("Expected parseVibrationEffect to fail for " + xml,
+ VibrationXmlParser.ParseFailedException.class,
+ () -> parseVibrationEffect(xml, /* flags= */ 0));
}
private void assertPublicApisParserSucceeds(String xml, VibrationEffect effect)
- throws IOException {
+ throws Exception {
+ assertPublicApisParseDocumentSucceeds(xml, effect);
+ assertPublicApisParseVibrationEffectSucceeds(xml, effect);
+ }
+
+ private void assertPublicApisParseDocumentSucceeds(String xml, VibrationEffect... effects)
+ throws Exception {
+ assertThat(parseDocument(xml, /* flags= */ 0))
+ .isEqualTo(new ParsedVibration(Arrays.asList(effects)));
+ }
+
+ private void assertPublicApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
+ throws Exception {
assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isEqualTo(effect);
}
+ private void assertHiddenApisParserFails(String xml) {
+ assertThrows("Expected parseVibrationEffect to fail for " + xml,
+ VibrationXmlParser.ParseFailedException.class,
+ () -> parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+ assertThrows("Expected parseDocument to fail for " + xml,
+ VibrationXmlParser.ParseFailedException.class,
+ () -> parseDocument(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+ }
+
+ private void assertHiddenApisParseVibrationEffectFails(String xml) {
+ assertThrows("Expected parseVibrationEffect to fail for " + xml,
+ VibrationXmlParser.ParseFailedException.class,
+ () -> parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+ }
+
+ private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect)
+ throws Exception {
+ assertHiddenApisParseDocumentSucceeds(xml, effect);
+ assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+ }
+
+ private void assertHiddenApisParseDocumentSucceeds(String xml, VibrationEffect... effect)
+ throws Exception {
+ assertThat(parseDocument(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+ .isEqualTo(new ParsedVibration(Arrays.asList(effect)));
+ }
+
+ private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
+ throws Exception {
+ assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+ .isEqualTo(effect);
+ }
+
+ private void assertPublicApisSerializerFails(VibrationEffect effect) {
+ assertThrows("Expected serialization to fail for " + effect,
+ VibrationXmlSerializer.SerializationFailedException.class,
+ () -> serialize(effect));
+ }
+
+ private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
+ String... expectedSegments) throws Exception {
+ assertSerializationContainsSegments(serialize(effect), expectedSegments);
+ }
+
+ private void assertHiddenApisSerializerSucceeds(VibrationEffect effect,
+ String... expectedSegments) throws Exception {
+ assertSerializationContainsSegments(
+ serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments);
+ }
+
+ private void assertPublicApisRoundTrip(VibrationEffect effect) throws Exception {
+ assertThat(parseVibrationEffect(serialize(effect, /* flags= */ 0), /* flags= */ 0))
+ .isEqualTo(effect);
+ }
+
+ private void assertHiddenApisRoundTrip(VibrationEffect effect) throws Exception {
+ String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
+ assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+ .isEqualTo(effect);
+ }
+
private TypedXmlPullParser createXmlPullParser(String xml) throws Exception {
TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
@@ -366,11 +523,6 @@
return parser;
}
- private void assertParseDocumentSucceeds(String xml, int flags, VibrationEffect... effects)
- throws Exception {
- assertThat(parseDocument(xml, flags).getVibrationEffects()).containsExactly(effects);
- }
-
/**
* Asserts parsing vibration from an open TypedXmlPullParser succeeds, and that the parser
* points to the end "vibration" or "vibration-select" tag.
@@ -385,7 +537,8 @@
String tagName = parser.getName();
assertThat(Set.of("vibration-effect", "vibration-select")).contains(tagName);
- assertThat(parseElement(parser, flags).getVibrationEffects()).containsExactly(effects);
+ assertThat(parseElement(parser, flags))
+ .isEqualTo(new ParsedVibration(Arrays.asList(effects)));
assertThat(parser.getEventType()).isEqualTo(XmlPullParser.END_TAG);
assertThat(parser.getName()).isEqualTo(tagName);
}
@@ -405,69 +558,40 @@
assertThat(parser.getEventType()).isEqualTo(parser.END_DOCUMENT);
}
- private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
- throws IOException {
- assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
- .isEqualTo(effect);
- }
-
- private void assertPublicApisSerializerFails(VibrationEffect effect) {
- assertThrows("Expected serialization to fail for " + effect,
- VibrationXmlSerializer.SerializationFailedException.class,
- () -> serialize(effect, /* flags= */ 0));
- }
-
private void assertParseElementFails(String xml) {
assertThrows("Expected parsing to fail for " + xml,
- VibrationXmlParser.VibrationXmlParserException.class,
+ VibrationXmlParser.ParseFailedException.class,
() -> parseElement(createXmlPullParser(xml), /* flags= */ 0));
}
- private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
- String... expectedSegments) throws IOException {
- assertSerializationContainsSegments(serialize(effect, /* flags= */ 0), expectedSegments);
- }
-
- private void assertHiddenApisSerializerSucceeds(VibrationEffect effect,
- String... expectedSegments) throws IOException {
- assertSerializationContainsSegments(
- serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments);
- }
-
private void assertSerializationContainsSegments(String xml, String[] expectedSegments) {
for (String expectedSegment : expectedSegments) {
assertThat(xml).contains(expectedSegment);
}
}
- private void assertPublicApisRoundTrip(VibrationEffect effect) throws IOException {
- assertThat(parseVibrationEffect(serialize(effect, /* flags= */ 0), /* flags= */ 0))
- .isEqualTo(effect);
- }
-
- private void assertHiddenApisRoundTrip(VibrationEffect effect) throws IOException {
- String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
- assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
- .isEqualTo(effect);
- }
-
private static VibrationEffect parseVibrationEffect(
- String xml, @VibrationXmlParser.Flags int flags) throws IOException {
+ String xml, @VibrationXmlParser.Flags int flags) throws Exception {
return VibrationXmlParser.parseVibrationEffect(new StringReader(xml), flags);
}
- private static ParsedVibration parseDocument(String xml, int flags)
- throws IOException {
+ private static ParsedVibration parseDocument(String xml, int flags) throws Exception {
return VibrationXmlParser.parseDocument(new StringReader(xml), flags);
}
private static ParsedVibration parseElement(TypedXmlPullParser parser, int flags)
- throws IOException, VibrationXmlParser.VibrationXmlParserException {
+ throws Exception {
return VibrationXmlParser.parseElement(parser, flags);
}
+ private static String serialize(VibrationEffect effect) throws Exception {
+ StringWriter writer = new StringWriter();
+ VibrationXmlSerializer.serialize(effect, writer);
+ return writer.toString();
+ }
+
private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags)
- throws IOException {
+ throws Exception {
StringWriter writer = new StringWriter();
VibrationXmlSerializer.serialize(effect, writer, flags);
return writer.toString();
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index 3256f31..5caedba 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -416,7 +416,8 @@
}
/**
- * Retrieve the dataspace associated with the texture image.
+ * Retrieve the dataspace associated with the texture image
+ * set by the most recent call to {@link #updateTexImage}.
*/
@SuppressLint("MethodNameUnits")
public @NamedDataSpace int getDataSpace() {
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index bcb1d29..52ae93f 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -29,6 +29,7 @@
android:name=".desktopmode.DesktopWallpaperActivity"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
+ android:showForAllUsers="true"
android:theme="@style/DesktopWallpaperTheme" />
<activity
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
index a3111b3..c9d3dbd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -324,6 +324,7 @@
enteringHasSameLetterbox = false
lastPostCommitFlingScale = SPRING_SCALE
gestureProgress = 0f
+ triggerBack = false
}
protected fun applyTransform(
@@ -499,10 +500,12 @@
}
override fun onBackCancelled() {
+ triggerBack = false
progressAnimator.onBackCancelled { finishAnimation() }
}
override fun onBackInvoked() {
+ triggerBack = true
progressAnimator.reset()
onGestureCommitted(progressAnimator.velocity)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 1279fc4..2aefc64 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -894,11 +894,22 @@
}
@Nullable
- Intent getAppBubbleIntent() {
+ @VisibleForTesting
+ public Intent getAppBubbleIntent() {
return mAppIntent;
}
/**
+ * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is
+ * true).
+ *
+ * @param appIntent The intent to set for the app bubble.
+ */
+ void setAppBubbleIntent(Intent appIntent) {
+ mAppIntent = appIntent;
+ }
+
+ /**
* Returns whether this bubble is from an app versus a notification.
*/
public boolean isAppBubble() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index d2c36e6..c853301 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1450,6 +1450,8 @@
if (b != null) {
// It's in the overflow, so remove it & reinflate
mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
+ // Update the bubble entry in the overflow with the latest intent.
+ b.setAppBubbleIntent(intent);
} else {
// App bubble does not exist, lets add and expand it
b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 87bd840..1fcfa7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -23,6 +23,7 @@
import android.os.UserManager;
import android.view.Choreographer;
import android.view.IWindowManager;
+import android.view.SurfaceControl;
import android.view.WindowManager;
import com.android.internal.jank.InteractionJankMonitor;
@@ -400,7 +401,8 @@
Optional<RecentTasksController> recentTasksController,
HomeTransitionObserver homeTransitionObserver) {
return new RecentsTransitionHandler(shellInit, transitions,
- recentTasksController.orElse(null), homeTransitionObserver);
+ recentTasksController.orElse(null), homeTransitionObserver,
+ SurfaceControl.Transaction::new);
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index 88d0554..5335c0b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -27,6 +27,8 @@
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
+import com.android.internal.jank.Cuj
+import com.android.wm.shell.common.InteractionJankMonitorUtils
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -103,6 +105,8 @@
onTaskResizeAnimationListener.onAnimationEnd(taskId)
finishCallback.onTransitionFinished(null)
boundsAnimator = null
+ InteractionJankMonitorUtils.endTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
}
)
addUpdateListener { anim ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 438aa76..b1cbe8d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -73,7 +73,7 @@
following system properties for example:
```shell
# Enabling
-adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods
adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface
adb reboot
adb logcat -s "SurfaceControlRegistry"
@@ -87,6 +87,16 @@
It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite
noisy if unfiltered.
+It can sometimes be useful to trace specific logs and when they are applied (sometimes we build
+transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to
+the set of requested calls:
+```shell
+# Enabling
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx
+adb reboot
+adb logcat -s "SurfaceControlRegistry"
+```
+
## Tracing activity starts in the app process
It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 03c8cf8..9f3c519 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -409,10 +409,6 @@
if (DesktopModeStatus.canEnterDesktopMode(mContext)
&& mDesktopModeTaskRepository.isPresent()
&& mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
- if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
- // Minimized freeform tasks should not be shown at all.
- continue;
- }
// Freeform tasks will be added as a separate entry
if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
mostRecentFreeformTaskIndex = recentTasks.size();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 3a266d9..c67cf1d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -74,6 +74,7 @@
import java.util.ArrayList;
import java.util.function.Consumer;
+import java.util.function.Supplier;
/**
* Handles the Recents (overview) animation. Only one of these can run at a time. A recents
@@ -84,6 +85,7 @@
private final Transitions mTransitions;
private final ShellExecutor mExecutor;
+ private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
@Nullable
private final RecentTasksController mRecentTasksController;
private IApplicationThread mAnimApp = null;
@@ -101,11 +103,13 @@
public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
@Nullable RecentTasksController recentTasksController,
- HomeTransitionObserver homeTransitionObserver) {
+ HomeTransitionObserver homeTransitionObserver,
+ Supplier<SurfaceControl.Transaction> transactionSupplier) {
mTransitions = transitions;
mExecutor = transitions.getMainExecutor();
mRecentTasksController = recentTasksController;
mHomeTransitionObserver = homeTransitionObserver;
+ mTransactionSupplier = transactionSupplier;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
if (recentTasksController == null) return;
shellInit.addInitCallback(() -> {
@@ -1056,7 +1060,7 @@
final Transitions.TransitionFinishCallback finishCB = mFinishCB;
mFinishCB = null;
- final SurfaceControl.Transaction t = mFinishTransaction;
+ SurfaceControl.Transaction t = mFinishTransaction;
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (mKeyguardLocked && mRecentsTask != null) {
@@ -1106,6 +1110,16 @@
}
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish");
+ if (toHome && !mOpeningTasks.isEmpty()) {
+ // Attempting to start a task after swipe to home, don't show it,
+ // move recents to top
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " attempting to start a task after swipe to home");
+ t = mTransactionSupplier.get();
+ wct.reorder(mRecentsTask, true /*onTop*/);
+ mClosingTasks.addAll(mOpeningTasks);
+ mOpeningTasks.clear();
+ }
// The general case: committing to recents, going home, or switching tasks.
for (int i = 0; i < mOpeningTasks.size(); ++i) {
t.show(mOpeningTasks.get(i).mTaskSurface);
@@ -1174,6 +1188,10 @@
mPipTransaction = null;
}
}
+ if (t != mFinishTransaction) {
+ // apply after merges because these changes are accounting for finishWCT changes.
+ mTransitions.setAfterMergeFinishTransaction(mTransition, t);
+ }
cleanUp();
finishCB.onTransitionFinished(wct.isEmpty() ? null : wct);
if (runnerFinishCb != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index b6a18e5..45eff4a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2649,7 +2649,7 @@
@Nullable TransitionRequestInfo request) {
final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
if (triggerTask == null) {
- if (isSplitActive()) {
+ if (isSplitScreenVisible()) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation",
request.getDebugId());
// Check if the display is rotating.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
index b03daaa..35427b9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
@@ -94,6 +94,11 @@
return rotatedBounds;
}
+ /** Returns true if the change is put on a surface in previous rotation. */
+ public boolean isRotated(@NonNull TransitionInfo.Change change) {
+ return mLastRotationDelta != 0 && mRotatorMap.containsKey(change.getParent());
+ }
+
/**
* Removes the counter rotation surface in the finish transaction. No need to reparent the
* children as the finish transaction should have already taken care of that.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 018c904..9412b2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -517,7 +517,8 @@
animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y);
}
- if (change.getActivityComponent() != null && !isActivityLevel) {
+ if (change.getActivityComponent() != null && !isActivityLevel
+ && !mRotator.isRotated(change)) {
// At this point, this is an independent activity change in a non-activity
// transition. This means that an activity transition got erroneously combined
// with another ongoing transition. This then means that the animation root may
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index f257e20..d2760ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -238,6 +238,13 @@
/** Ordered list of transitions which have been merged into this one. */
private ArrayList<ActiveTransition> mMerged;
+ /**
+ * @deprecated DO NOT USE THIS unless absolutely necessary. It will be removed once
+ * everything migrates off finishWCT.
+ */
+ @java.lang.Deprecated
+ SurfaceControl.Transaction mAfterMergeFinishT;
+
ActiveTransition(IBinder token) {
mToken = token;
}
@@ -1018,6 +1025,20 @@
return null;
}
+ /** @deprecated */
+ @java.lang.Deprecated
+ public void setAfterMergeFinishTransaction(IBinder transition,
+ SurfaceControl.Transaction afterMergeFinishT) {
+ final ActiveTransition at = mKnownTransitions.get(transition);
+ if (at == null) return;
+ if (at.mAfterMergeFinishT != null) {
+ Log.e(TAG, "Setting after-merge-t >1 time on transition: " + at.mInfo.getDebugId());
+ at.mAfterMergeFinishT.merge(afterMergeFinishT);
+ return;
+ }
+ at.mAfterMergeFinishT = afterMergeFinishT;
+ }
+
/** Aborts a transition. This will still queue it up to maintain order. */
private void onAbort(ActiveTransition transition) {
final Track track = mTracks.get(transition.getTrack());
@@ -1078,6 +1099,7 @@
}
// Merge all associated transactions together
SurfaceControl.Transaction fullFinish = active.mFinishT;
+ SurfaceControl.Transaction afterMergeFinish = active.mAfterMergeFinishT;
if (active.mMerged != null) {
for (int iM = 0; iM < active.mMerged.size(); ++iM) {
final ActiveTransition toMerge = active.mMerged.get(iM);
@@ -1097,6 +1119,21 @@
fullFinish.merge(toMerge.mFinishT);
}
}
+ if (toMerge.mAfterMergeFinishT != null) {
+ if (afterMergeFinish == null) {
+ afterMergeFinish = toMerge.mAfterMergeFinishT;
+ } else {
+ afterMergeFinish.merge(toMerge.mAfterMergeFinishT);
+ }
+ toMerge.mAfterMergeFinishT = null;
+ }
+ }
+ }
+ if (afterMergeFinish != null) {
+ if (fullFinish == null) {
+ fullFinish = afterMergeFinish;
+ } else {
+ fullFinish.merge(afterMergeFinish);
}
}
if (fullFinish != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 5e7e5e6..e1009a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -73,6 +73,7 @@
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.Cuj;
import com.android.internal.protolog.common.ProtoLog;
import com.android.window.flags.Flags;
import com.android.wm.shell.R;
@@ -81,6 +82,7 @@
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.InteractionJankMonitorUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
@@ -470,11 +472,17 @@
} else if (id == R.id.collapse_menu_button) {
decoration.closeHandleMenu();
} else if (id == R.id.maximize_window) {
+ InteractionJankMonitorUtils.beginTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
+ /* tag= */ "caption_bar_button");
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
} else if (id == R.id.maximize_menu_maximize_button) {
+ InteractionJankMonitorUtils.beginTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
+ /* tag= */ "maximize_menu_option");
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
decoration.closeHandleMenu();
@@ -712,6 +720,9 @@
return false;
}
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ InteractionJankMonitorUtils.beginTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext,
+ /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap");
mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index fe1c9c3..d48ce53 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -28,6 +28,8 @@
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
+import androidx.annotation.NonNull;
+
import com.android.window.flags.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayController;
@@ -106,13 +108,15 @@
repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom)
? candidateBottom : oldBottom;
}
- // If width or height are negative or less than the minimum width or height, revert the
+ // If width or height are negative or exceeding the width or height constraints, revert the
// respective bounds to use previous bound dimensions.
- if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+ if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController,
+ windowDecoration)) {
repositionTaskBounds.right = oldRight;
repositionTaskBounds.left = oldLeft;
}
- if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+ if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController,
+ windowDecoration)) {
repositionTaskBounds.top = oldTop;
repositionTaskBounds.bottom = oldBottom;
}
@@ -174,6 +178,30 @@
return result;
}
+ private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds,
+ Rect maxResizeBounds, DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ // Check if width is less than the minimum width constraint.
+ if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+ return true;
+ }
+ // Check if width is more than the maximum resize bounds on desktop windowing mode.
+ return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
+ && repositionTaskBounds.width() > maxResizeBounds.width();
+ }
+
+ private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds,
+ Rect maxResizeBounds, DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ // Check if height is less than the minimum height constraint.
+ if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+ return true;
+ }
+ // Check if height is more than the maximum resize bounds on desktop windowing mode.
+ return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
+ && repositionTaskBounds.height() > maxResizeBounds.height();
+ }
+
private static float getMinWidth(DisplayController displayController,
WindowDecoration windowDecoration) {
return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController,
@@ -210,7 +238,7 @@
private static float getDefaultMinSize(DisplayController displayController,
WindowDecoration windowDecoration) {
- float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
+ float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
.densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
return windowDecoration.mTaskInfo.defaultMinSize * density;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index e291c0e..5c5a1a2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -399,7 +399,7 @@
}
@Test
- public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() {
+ public void testGetRecentTasks_proto2Enabled_includesMinimizedFreeformTasks() {
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -415,8 +415,7 @@
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
- // 2 freeform tasks should be grouped into one, 1 task should be skipped, 3 total recents
- // entries
+ // 3 freeform tasks should be grouped into one, 2 single tasks, 3 total recents entries
assertEquals(3, recentTasks.size());
GroupedRecentTaskInfo freeformGroup = recentTasks.get(0);
GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1);
@@ -428,9 +427,10 @@
assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType());
// Check freeform group entries
- assertEquals(2, freeformGroup.getTaskInfoList().size());
+ assertEquals(3, freeformGroup.getTaskInfoList().size());
assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
- assertEquals(t5, freeformGroup.getTaskInfoList().get(1));
+ assertEquals(t3, freeformGroup.getTaskInfoList().get(1));
+ assertEquals(t5, freeformGroup.getTaskInfoList().get(2));
// Check single entries
assertEquals(t2, singleGroup1.getTaskInfo1());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 964d86e..69a61ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1192,7 +1192,8 @@
mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, transitions,
- mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+ mock(RecentTasksController.class), mock(HomeTransitionObserver.class),
+ () -> mock(SurfaceControl.Transaction.class));
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
shellInit.init();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index f750e6b..86aded7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -21,7 +21,9 @@
import android.graphics.PointF
import android.graphics.Rect
import android.os.IBinder
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display
import android.window.WindowContainerToken
@@ -36,6 +38,7 @@
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
@@ -53,21 +56,32 @@
class DragPositioningCallbackUtilityTest {
@Mock
private lateinit var mockWindowDecoration: WindowDecoration<*>
+
@Mock
private lateinit var taskToken: WindowContainerToken
+
@Mock
private lateinit var taskBinder: IBinder
+
@Mock
private lateinit var mockDisplayController: DisplayController
+
@Mock
private lateinit var mockDisplayLayout: DisplayLayout
+
@Mock
private lateinit var mockDisplay: Display
+
@Mock
private lateinit var mockContext: Context
+
@Mock
private lateinit var mockResources: Resources
+ @JvmField
+ @Rule
+ val setFlagsRule = SetFlagsRule()
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -323,6 +337,49 @@
assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50)
}
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+ fun testChangeBounds_windowSizeExceedsStableBounds_shouldBeAllowedToChangeBounds() {
+ val startingPoint =
+ PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(),
+ OFF_CENTER_STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS)
+ // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach
+ // the disallowed drag area.
+ val offset = 5
+ val newX = STABLE_BOUNDS.right.toFloat() - offset
+ val newY = STABLE_BOUNDS.bottom.toFloat() - offset
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+ assertThat(repositionTaskBounds.width()).isGreaterThan(STABLE_BOUNDS.right)
+ assertThat(repositionTaskBounds.height()).isGreaterThan(STABLE_BOUNDS.bottom)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+ fun testChangeBoundsInDesktopMode_windowSizeExceedsStableBounds_shouldBeLimitedToDisplaySize() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true)
+ val startingPoint =
+ PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(),
+ OFF_CENTER_STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS)
+ // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach
+ // the disallowed drag area.
+ val offset = 5
+ val newX = STABLE_BOUNDS.right.toFloat() - offset
+ val newY = STABLE_BOUNDS.bottom.toFloat() - offset
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+ assertThat(repositionTaskBounds.width()).isLessThan(STABLE_BOUNDS.right)
+ assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom)
+ }
+
private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) {
mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
taskId = TASK_ID
@@ -347,6 +404,7 @@
private const val NAVBAR_HEIGHT = 50
private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+ private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10)
private val DISALLOWED_RESIZE_AREA = Rect(
DISPLAY_BOUNDS.left,
DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 48ac1e5..901ca90 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -17,6 +17,8 @@
import android.app.ActivityManager
import android.app.WindowConfiguration
+import android.content.Context
+import android.content.res.Resources
import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
@@ -98,6 +100,10 @@
private lateinit var mockFinishCallback: TransitionFinishCallback
@Mock
private lateinit var mockTransitions: Transitions
+ @Mock
+ private lateinit var mockContext: Context
+ @Mock
+ private lateinit var mockResources: Resources
private lateinit var taskPositioner: VeiledResizeTaskPositioner
@@ -105,6 +111,9 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ mockDesktopWindowDecoration.mDisplay = mockDisplay
+ mockDesktopWindowDecoration.mDecorWindowContext = mockContext
+ whenever(mockContext.getResources()).thenReturn(mockResources)
whenever(taskToken.asBinder()).thenReturn(taskBinder)
whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
diff --git a/libs/nativehelper_jvm/Android.bp b/libs/nativehelper_jvm/Android.bp
new file mode 100644
index 0000000..b5b7028
--- /dev/null
+++ b/libs/nativehelper_jvm/Android.bp
@@ -0,0 +1,19 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+cc_library_host_static {
+ name: "libnativehelper_jvm",
+ srcs: [
+ "JNIPlatformHelp.c",
+ "JniConstants.c",
+ "file_descriptor_jni.c",
+ ],
+ whole_static_libs: ["libnativehelper_any_vm"],
+ export_static_lib_headers: ["libnativehelper_any_vm"],
+ target: {
+ windows: {
+ enabled: true,
+ },
+ },
+}
diff --git a/libs/nativehelper_jvm/JNIPlatformHelp.c b/libs/nativehelper_jvm/JNIPlatformHelp.c
new file mode 100644
index 0000000..9df31a8
--- /dev/null
+++ b/libs/nativehelper_jvm/JNIPlatformHelp.c
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIPlatformHelp.h>
+
+#include <stddef.h>
+
+#include "JniConstants.h"
+
+static int GetBufferPosition(JNIEnv* env, jobject nioBuffer) {
+ return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_position(env));
+}
+
+static int GetBufferLimit(JNIEnv* env, jobject nioBuffer) {
+ return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_limit(env));
+}
+
+static int GetBufferElementSizeShift(JNIEnv* env, jobject nioBuffer) {
+ jclass byteBufferClass = JniConstants_NioByteBufferClass(env);
+ jclass shortBufferClass = JniConstants_NioShortBufferClass(env);
+ jclass charBufferClass = JniConstants_NioCharBufferClass(env);
+ jclass intBufferClass = JniConstants_NioIntBufferClass(env);
+ jclass floatBufferClass = JniConstants_NioFloatBufferClass(env);
+ jclass longBufferClass = JniConstants_NioLongBufferClass(env);
+ jclass doubleBufferClass = JniConstants_NioDoubleBufferClass(env);
+
+ // Check the type of the Buffer
+ if ((*env)->IsInstanceOf(env, nioBuffer, byteBufferClass)) {
+ return 0;
+ } else if ((*env)->IsInstanceOf(env, nioBuffer, shortBufferClass) ||
+ (*env)->IsInstanceOf(env, nioBuffer, charBufferClass)) {
+ return 1;
+ } else if ((*env)->IsInstanceOf(env, nioBuffer, intBufferClass) ||
+ (*env)->IsInstanceOf(env, nioBuffer, floatBufferClass)) {
+ return 2;
+ } else if ((*env)->IsInstanceOf(env, nioBuffer, longBufferClass) ||
+ (*env)->IsInstanceOf(env, nioBuffer, doubleBufferClass)) {
+ return 3;
+ }
+ return 0;
+}
+
+jarray jniGetNioBufferBaseArray(JNIEnv* env, jobject nioBuffer) {
+ jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env);
+ jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod);
+ if (hasArray) {
+ jmethodID arrayMethod = JniConstants_NioBuffer_array(env);
+ return (*env)->CallObjectMethod(env, nioBuffer, arrayMethod);
+ } else {
+ return NULL;
+ }
+}
+
+int jniGetNioBufferBaseArrayOffset(JNIEnv* env, jobject nioBuffer) {
+ jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env);
+ jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod);
+ if (hasArray) {
+ jmethodID arrayOffsetMethod = JniConstants_NioBuffer_arrayOffset(env);
+ jint arrayOffset = (*env)->CallIntMethod(env, nioBuffer, arrayOffsetMethod);
+ const int position = GetBufferPosition(env, nioBuffer);
+ jint elementSizeShift = GetBufferElementSizeShift(env, nioBuffer);
+ return (arrayOffset + position) << elementSizeShift;
+ } else {
+ return 0;
+ }
+}
+
+jlong jniGetNioBufferPointer(JNIEnv* env, jobject nioBuffer) {
+ // in Java 11, the address field of a HeapByteBuffer contains a non-zero value despite
+ // HeapByteBuffer being a non-direct buffer. In that case, this should still return 0.
+ jmethodID isDirectMethod = JniConstants_NioBuffer_isDirect(env);
+ jboolean isDirect = (*env)->CallBooleanMethod(env, nioBuffer, isDirectMethod);
+ if (isDirect == JNI_FALSE) {
+ return 0L;
+ }
+ jlong baseAddress = (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env));
+ if (baseAddress != 0) {
+ const int position = GetBufferPosition(env, nioBuffer);
+ const int shift = GetBufferElementSizeShift(env, nioBuffer);
+ baseAddress += position << shift;
+ }
+ return baseAddress;
+}
+
+jlong jniGetNioBufferFields(JNIEnv* env, jobject nioBuffer,
+ jint* position, jint* limit, jint* elementSizeShift) {
+ *position = GetBufferPosition(env, nioBuffer);
+ *limit = GetBufferLimit(env, nioBuffer);
+ *elementSizeShift = GetBufferElementSizeShift(env, nioBuffer);
+ return (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env));
+}
diff --git a/libs/nativehelper_jvm/JniConstants.c b/libs/nativehelper_jvm/JniConstants.c
new file mode 100644
index 0000000..ca58f61
--- /dev/null
+++ b/libs/nativehelper_jvm/JniConstants.c
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+#include "JniConstants.h"
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#define LOG_TAG "JniConstants"
+#include <log/log.h>
+
+// jclass constants list:
+// <class, signature, androidOnly>
+#define JCLASS_CONSTANTS_LIST(V) \
+ V(FileDescriptor, "java/io/FileDescriptor", false) \
+ V(NioBuffer, "java/nio/Buffer", false) \
+ V(NioByteBuffer, "java/nio/ByteBuffer", false) \
+ V(NioShortBuffer, "java/nio/ShortBuffer", false) \
+ V(NioCharBuffer, "java/nio/CharBuffer", false) \
+ V(NioIntBuffer, "java/nio/IntBuffer", false) \
+ V(NioFloatBuffer, "java/nio/FloatBuffer", false) \
+ V(NioLongBuffer, "java/nio/LongBuffer", false) \
+ V(NioDoubleBuffer, "java/nio/DoubleBuffer", false)
+
+// jmethodID's of public methods constants list:
+// <Class, method, method-string, signature, is_static>
+#define JMETHODID_CONSTANTS_LIST(V) \
+ V(FileDescriptor, init, "<init>", "()V", false) \
+ V(NioBuffer, array, "array", "()Ljava/lang/Object;", false) \
+ V(NioBuffer, hasArray, "hasArray", "()Z", false) \
+ V(NioBuffer, isDirect, "isDirect", "()Z", false) \
+ V(NioBuffer, arrayOffset, "arrayOffset", "()I", false)
+
+// jfieldID constants list:
+// <Class, field, signature, is_static>
+#define JFIELDID_CONSTANTS_LIST(V) \
+ V(FileDescriptor, fd, "I", false) \
+ V(NioBuffer, address, "J", false) \
+ V(NioBuffer, limit, "I", false) \
+ V(NioBuffer, position, "I", false)
+
+#define CLASS_NAME(cls) g_ ## cls
+#define METHOD_NAME(cls, method) g_ ## cls ## _ ## method
+#define FIELD_NAME(cls, field) g_ ## cls ## _ ## field
+
+//
+// Declare storage for cached classes, methods and fields.
+//
+
+#define JCLASS_DECLARE_STORAGE(cls, ...) \
+ static jclass CLASS_NAME(cls) = NULL;
+JCLASS_CONSTANTS_LIST(JCLASS_DECLARE_STORAGE)
+#undef JCLASS_DECLARE_STORAGE
+
+#define JMETHODID_DECLARE_STORAGE(cls, method, ...) \
+ static jmethodID METHOD_NAME(cls, method) = NULL;
+JMETHODID_CONSTANTS_LIST(JMETHODID_DECLARE_STORAGE)
+#undef JMETHODID_DECLARE_STORAGE
+
+#define JFIELDID_DECLARE_STORAGE(cls, field, ...) \
+ static jfieldID FIELD_NAME(cls, field) = NULL;
+JFIELDID_CONSTANTS_LIST(JFIELDID_DECLARE_STORAGE)
+#undef JFIELDID_DECLARE_STORAGE
+
+//
+// Helper methods
+//
+
+static jclass FindClass(JNIEnv* env, const char* signature, bool androidOnly) {
+ jclass cls = (*env)->FindClass(env, signature);
+ if (cls == NULL) {
+ LOG_ALWAYS_FATAL_IF(!androidOnly, "Class not found: %s", signature);
+ return NULL;
+ }
+ return (*env)->NewGlobalRef(env, cls);
+}
+
+static jmethodID FindMethod(JNIEnv* env, jclass cls,
+ const char* name, const char* signature, bool isStatic) {
+ jmethodID method;
+ if (isStatic) {
+ method = (*env)->GetStaticMethodID(env, cls, name, signature);
+ } else {
+ method = (*env)->GetMethodID(env, cls, name, signature);
+ }
+ LOG_ALWAYS_FATAL_IF(method == NULL, "Method not found: %s:%s", name, signature);
+ return method;
+}
+
+static jfieldID FindField(JNIEnv* env, jclass cls,
+ const char* name, const char* signature, bool isStatic) {
+ jfieldID field;
+ if (isStatic) {
+ field = (*env)->GetStaticFieldID(env, cls, name, signature);
+ } else {
+ field = (*env)->GetFieldID(env, cls, name, signature);
+ }
+ LOG_ALWAYS_FATAL_IF(field == NULL, "Field not found: %s:%s", name, signature);
+ return field;
+}
+
+static pthread_once_t g_initialized = PTHREAD_ONCE_INIT;
+static JNIEnv* g_init_env;
+
+static void InitializeConstants() {
+ // Initialize cached classes.
+#define JCLASS_INITIALIZE(cls, signature, androidOnly) \
+ CLASS_NAME(cls) = FindClass(g_init_env, signature, androidOnly);
+ JCLASS_CONSTANTS_LIST(JCLASS_INITIALIZE)
+#undef JCLASS_INITIALIZE
+
+ // Initialize cached methods.
+#define JMETHODID_INITIALIZE(cls, method, name, signature, isStatic) \
+ METHOD_NAME(cls, method) = \
+ FindMethod(g_init_env, CLASS_NAME(cls), name, signature, isStatic);
+ JMETHODID_CONSTANTS_LIST(JMETHODID_INITIALIZE)
+#undef JMETHODID_INITIALIZE
+
+ // Initialize cached fields.
+#define JFIELDID_INITIALIZE(cls, field, signature, isStatic) \
+ FIELD_NAME(cls, field) = \
+ FindField(g_init_env, CLASS_NAME(cls), #field, signature, isStatic);
+ JFIELDID_CONSTANTS_LIST(JFIELDID_INITIALIZE)
+#undef JFIELDID_INITIALIZE
+}
+
+void EnsureInitialized(JNIEnv* env) {
+ // This method has to be called in every cache accesses because library can be built
+ // 2 different ways and existing usage for compat version doesn't have a good hook for
+ // initialization and is widely used.
+ g_init_env = env;
+ pthread_once(&g_initialized, InitializeConstants);
+}
+
+// API exported by libnativehelper_api.h.
+
+void jniUninitializeConstants() {
+ // Uninitialize cached classes, methods and fields.
+ //
+ // NB we assume the runtime is stopped at this point and do not delete global
+ // references.
+#define JCLASS_INVALIDATE(cls, ...) CLASS_NAME(cls) = NULL;
+ JCLASS_CONSTANTS_LIST(JCLASS_INVALIDATE);
+#undef JCLASS_INVALIDATE
+
+#define JMETHODID_INVALIDATE(cls, method, ...) METHOD_NAME(cls, method) = NULL;
+ JMETHODID_CONSTANTS_LIST(JMETHODID_INVALIDATE);
+#undef JMETHODID_INVALIDATE
+
+#define JFIELDID_INVALIDATE(cls, field, ...) FIELD_NAME(cls, field) = NULL;
+ JFIELDID_CONSTANTS_LIST(JFIELDID_INVALIDATE);
+#undef JFIELDID_INVALIDATE
+
+ // If jniConstantsUninitialize is called, runtime has shutdown. Reset
+ // state as some tests re-start the runtime.
+ pthread_once_t o = PTHREAD_ONCE_INIT;
+ memcpy(&g_initialized, &o, sizeof(o));
+}
+
+//
+// Accessors
+//
+
+#define JCLASS_ACCESSOR_IMPL(cls, ...) \
+jclass JniConstants_ ## cls ## Class(JNIEnv* env) { \
+ EnsureInitialized(env); \
+ return CLASS_NAME(cls); \
+}
+JCLASS_CONSTANTS_LIST(JCLASS_ACCESSOR_IMPL)
+#undef JCLASS_ACCESSOR_IMPL
+
+#define JMETHODID_ACCESSOR_IMPL(cls, method, ...) \
+jmethodID JniConstants_ ## cls ## _ ## method(JNIEnv* env) { \
+ EnsureInitialized(env); \
+ return METHOD_NAME(cls, method); \
+}
+JMETHODID_CONSTANTS_LIST(JMETHODID_ACCESSOR_IMPL)
+
+#define JFIELDID_ACCESSOR_IMPL(cls, field, ...) \
+jfieldID JniConstants_ ## cls ## _ ## field(JNIEnv* env) { \
+ EnsureInitialized(env); \
+ return FIELD_NAME(cls, field); \
+}
+JFIELDID_CONSTANTS_LIST(JFIELDID_ACCESSOR_IMPL)
diff --git a/libs/nativehelper_jvm/JniConstants.h b/libs/nativehelper_jvm/JniConstants.h
new file mode 100644
index 0000000..e7a266d
--- /dev/null
+++ b/libs/nativehelper_jvm/JniConstants.h
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+
+#include <jni.h>
+
+__BEGIN_DECLS
+
+//
+// Classes in constants cache.
+//
+// NB The implementations of these methods are generated by the JCLASS_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jclass JniConstants_FileDescriptorClass(JNIEnv* env);
+jclass JniConstants_NioByteBufferClass(JNIEnv* env);
+jclass JniConstants_NioShortBufferClass(JNIEnv* env);
+jclass JniConstants_NioCharBufferClass(JNIEnv* env);
+jclass JniConstants_NioIntBufferClass(JNIEnv* env);
+jclass JniConstants_NioFloatBufferClass(JNIEnv* env);
+jclass JniConstants_NioLongBufferClass(JNIEnv* env);
+jclass JniConstants_NioDoubleBufferClass(JNIEnv* env);
+
+//
+// Methods in the constants cache.
+//
+// NB The implementations of these methods are generated by the JMETHODID_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jmethodID JniConstants_FileDescriptor_init(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_array(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_arrayOffset(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_hasArray(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_isDirect(JNIEnv* env);
+
+//
+// Fields in the constants cache.
+//
+// NB The implementations of these methods are generated by the JFIELDID_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jfieldID JniConstants_FileDescriptor_fd(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_address(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_limit(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_position(JNIEnv* env);
+
+__END_DECLS
diff --git a/libs/nativehelper_jvm/OWNERS b/libs/nativehelper_jvm/OWNERS
new file mode 100644
index 0000000..5d55f6e
--- /dev/null
+++ b/libs/nativehelper_jvm/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 326772
+
+include /libs/hwui/OWNERS
+include platform/libnativehelper:/OWNERS
+
+diegoperez@google.com
+jgaillard@google.com
diff --git a/libs/nativehelper_jvm/README b/libs/nativehelper_jvm/README
new file mode 100644
index 0000000..755c422
--- /dev/null
+++ b/libs/nativehelper_jvm/README
@@ -0,0 +1,2 @@
+libnativehelper_jvm is a JVM-compatible version of libnativehelper.
+It should be used instead of libnativehelper whenever a host library is meant to run on a JVM.
\ No newline at end of file
diff --git a/libs/nativehelper_jvm/file_descriptor_jni.c b/libs/nativehelper_jvm/file_descriptor_jni.c
new file mode 100644
index 0000000..36880cd
--- /dev/null
+++ b/libs/nativehelper_jvm/file_descriptor_jni.c
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#include <android/file_descriptor_jni.h>
+
+#include <stddef.h>
+
+#define LOG_TAG "file_descriptor_jni"
+#include <log/log.h>
+
+#include "JniConstants.h"
+
+static void EnsureArgumentIsFileDescriptor(JNIEnv* env, jobject instance) {
+ LOG_ALWAYS_FATAL_IF(instance == NULL, "FileDescriptor is NULL");
+ jclass jifd = JniConstants_FileDescriptorClass(env);
+ LOG_ALWAYS_FATAL_IF(!(*env)->IsInstanceOf(env, instance, jifd),
+ "Argument is not a FileDescriptor");
+}
+
+JNIEXPORT _Nullable jobject AFileDescriptor_create(JNIEnv* env) {
+ return (*env)->NewObject(env,
+ JniConstants_FileDescriptorClass(env),
+ JniConstants_FileDescriptor_init(env));
+}
+
+JNIEXPORT int AFileDescriptor_getFd(JNIEnv* env, jobject fileDescriptor) {
+ EnsureArgumentIsFileDescriptor(env, fileDescriptor);
+ return (*env)->GetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env));
+}
+
+JNIEXPORT void AFileDescriptor_setFd(JNIEnv* env, jobject fileDescriptor, int fd) {
+ EnsureArgumentIsFileDescriptor(env, fileDescriptor);
+ (*env)->SetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env), fd);
+}
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 746c280..8f16f76 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -23,6 +23,9 @@
cc_library_shared {
name: "libjnigraphics",
+ defaults: [
+ "bug_24465209_workaround",
+ ],
cflags: [
"-Wall",
@@ -47,13 +50,6 @@
static_libs: ["libarect"],
- arch: {
- arm: {
- // TODO: This is to work around b/24465209. Remove after root cause is fixed
- pack_relocations: false,
- ldflags: ["-Wl,--hash-style=both"],
- },
- },
host_supported: true,
target: {
android: {
diff --git a/packages/CtsShim/Android.bp b/packages/CtsShim/Android.bp
index baafe7b..a94c8c5 100644
--- a/packages/CtsShim/Android.bp
+++ b/packages/CtsShim/Android.bp
@@ -61,7 +61,6 @@
"com.android.apex.cts.shim.v1",
"com.android.apex.cts.shim.v2",
"com.android.apex.cts.shim.v2_legacy",
- "com.android.apex.cts.shim.v2_no_hashtree",
"com.android.apex.cts.shim.v2_sdk_target_p",
"com.android.apex.cts.shim.v3",
],
@@ -102,7 +101,6 @@
"com.android.apex.cts.shim.v1",
"com.android.apex.cts.shim.v2",
"com.android.apex.cts.shim.v2_legacy",
- "com.android.apex.cts.shim.v2_no_hashtree",
"com.android.apex.cts.shim.v2_sdk_target_p",
"com.android.apex.cts.shim.v3",
],
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index d6b7ecf..5b3d47e 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -93,7 +93,6 @@
"com.android.apex.cts.shim.v1",
"com.android.apex.cts.shim.v2",
"com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
- "com.android.apex.cts.shim.v2_no_hashtree",
"com.android.apex.cts.shim.v2_legacy",
"com.android.apex.cts.shim.v2_sdk_target_p",
"com.android.apex.cts.shim.v2_unsigned_payload",
@@ -200,7 +199,6 @@
"com.android.apex.cts.shim.v1",
"com.android.apex.cts.shim.v2",
"com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
- "com.android.apex.cts.shim.v2_no_hashtree",
"com.android.apex.cts.shim.v2_legacy",
"com.android.apex.cts.shim.v2_sdk_target_p",
"com.android.apex.cts.shim.v2_unsigned_payload",
diff --git a/packages/CtsShim/build/jni/Android.bp b/packages/CtsShim/build/jni/Android.bp
index 2dbf2a2..ac85d2b 100644
--- a/packages/CtsShim/build/jni/Android.bp
+++ b/packages/CtsShim/build/jni/Android.bp
@@ -33,7 +33,6 @@
"com.android.apex.cts.shim.v1",
"com.android.apex.cts.shim.v2",
"com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
- "com.android.apex.cts.shim.v2_no_hashtree",
"com.android.apex.cts.shim.v2_legacy",
"com.android.apex.cts.shim.v2_sdk_target_p",
"com.android.apex.cts.shim.v2_unsigned_payload",
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index 62ed66c..e4bc7b4 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -13,4 +13,4 @@
per-file *.xml=*
# Notification-related utilities
-per-file */notification/* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+per-file **/notification/** = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
diff --git a/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
new file mode 100644
index 0000000..06c0d8c
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
@@ -0,0 +1,28 @@
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8c4.41,0 8,3.59 8,8C20,16.41 16.41,20 12,20z"/>
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M7,11h10v2h-10z"/>
+</vector>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index d373201..27c386e 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1361,6 +1361,9 @@
<!-- Keywords for setting screen for controlling apps that can schedule alarms [CHAR LIMIT=100] -->
<string name="keywords_alarms_and_reminders">schedule, alarm, reminder, clock</string>
+ <!-- Sound: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
+ <string name="zen_mode_settings_title">Do Not Disturb</string>
+
<!-- Do not disturb: Label for button in enable zen dialog that will turn on zen mode. [CHAR LIMIT=30] -->
<string name="zen_mode_enable_dialog_turn_on">Turn on</string>
<!-- Do not disturb: Title for the Do not Disturb dialog to turn on Do not disturb. [CHAR LIMIT=50]-->
@@ -1387,6 +1390,9 @@
<!-- Do not disturb: Duration option to always have DND on until it is manually turned off [CHAR LIMIT=60] -->
<string name="zen_mode_forever">Until you turn off</string>
+ <!-- [CHAR LIMIT=50] Zen mode settings: placeholder for a Contact name when the name is empty -->
+ <string name="zen_mode_starred_contacts_empty_name">(No name)</string>
+
<!-- time label for event have that happened very recently [CHAR LIMIT=60] -->
<string name="time_unit_just_now">Just now</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index 1597a4b..fc163ce 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -430,13 +430,21 @@
return null;
}
+ /**
+ * Retrieves the user ID of a managed profile associated with a specific user.
+ *
+ * <p>This method iterates over the users in the profile group associated with the given user ID
+ * and returns the ID of the user that is identified as a managed profile user.
+ * If no managed profile is found, it returns {@link UserHandle#USER_NULL}.
+ *
+ * @param context The context used to obtain the {@link UserManager} system service.
+ * @param userId The ID of the user for whom to find the managed profile.
+ * @return The user ID of the managed profile, or {@link UserHandle#USER_NULL} if none exists.
+ */
private static int getManagedProfileId(Context context, int userId) {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
List<UserInfo> userProfiles = um.getProfiles(userId);
for (UserInfo uInfo : userProfiles) {
- if (uInfo.id == userId) {
- continue;
- }
if (uInfo.isManagedProfile()) {
return uInfo.id;
}
@@ -821,11 +829,11 @@
}
EnforcedAdmin admin =
RestrictedLockUtils.getProfileOrDeviceOwner(
- context, UserHandle.of(UserHandle.USER_SYSTEM));
+ context, context.getUser());
if (admin != null) {
return admin;
}
- int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
+ int profileId = getManagedProfileId(context, context.getUserId());
return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
}
@@ -848,7 +856,7 @@
if (admin != null) {
return admin;
}
- int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
+ int profileId = getManagedProfileId(context, context.getUserId());
return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
new file mode 100644
index 0000000..3f19830
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
@@ -0,0 +1,160 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.service.notification.SystemZenRules;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ZenIconLoader {
+
+ private static final String TAG = "ZenIconLoader";
+
+ private static final Drawable MISSING = new ColorDrawable();
+
+ @Nullable // Until first usage
+ private static ZenIconLoader sInstance;
+
+ private final LruCache<String, Drawable> mCache;
+ private final ListeningExecutorService mBackgroundExecutor;
+
+ public static ZenIconLoader getInstance() {
+ if (sInstance == null) {
+ sInstance = new ZenIconLoader();
+ }
+ return sInstance;
+ }
+
+ private ZenIconLoader() {
+ this(Executors.newFixedThreadPool(4));
+ }
+
+ @VisibleForTesting
+ ZenIconLoader(ExecutorService backgroundExecutor) {
+ mCache = new LruCache<>(50);
+ mBackgroundExecutor =
+ MoreExecutors.listeningDecorator(backgroundExecutor);
+ }
+
+ @NonNull
+ ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
+ if (rule.getIconResId() == 0) {
+ return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
+ }
+
+ return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
+ .transform(icon ->
+ icon != null ? icon : getFallbackIcon(context, rule.getType()),
+ MoreExecutors.directExecutor());
+ }
+
+ @NonNull
+ private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
+ int iconResId) {
+ String cacheKey = pkg + ":" + iconResId;
+ synchronized (mCache) {
+ Drawable cachedValue = mCache.get(cacheKey);
+ if (cachedValue != null) {
+ return immediateFuture(cachedValue != MISSING ? cachedValue : null);
+ }
+ }
+
+ return FluentFuture.from(mBackgroundExecutor.submit(() -> {
+ if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
+ return context.getDrawable(iconResId);
+ } else {
+ Context appContext = context.createPackageContext(pkg, 0);
+ Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
+ return getMonochromeIconIfPresent(appDrawable);
+ }
+ })).catching(Exception.class, ex -> {
+ // If we cannot resolve the icon, then store MISSING in the cache below, so
+ // we don't try again.
+ Log.e(TAG, "Error while loading icon " + cacheKey, ex);
+ return null;
+ }, MoreExecutors.directExecutor()).transform(drawable -> {
+ synchronized (mCache) {
+ mCache.put(cacheKey, drawable != null ? drawable : MISSING);
+ }
+ return drawable;
+ }, MoreExecutors.directExecutor());
+ }
+
+ private static Drawable getFallbackIcon(Context context, int ruleType) {
+ int iconResIdFromType = switch (ruleType) {
+ case AutomaticZenRule.TYPE_UNKNOWN ->
+ com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+ case AutomaticZenRule.TYPE_OTHER ->
+ com.android.internal.R.drawable.ic_zen_mode_type_other;
+ case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
+ com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
+ case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
+ com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
+ case AutomaticZenRule.TYPE_BEDTIME ->
+ com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
+ case AutomaticZenRule.TYPE_DRIVING ->
+ com.android.internal.R.drawable.ic_zen_mode_type_driving;
+ case AutomaticZenRule.TYPE_IMMERSIVE ->
+ com.android.internal.R.drawable.ic_zen_mode_type_immersive;
+ case AutomaticZenRule.TYPE_THEATER ->
+ com.android.internal.R.drawable.ic_zen_mode_type_theater;
+ case AutomaticZenRule.TYPE_MANAGED ->
+ com.android.internal.R.drawable.ic_zen_mode_type_managed;
+ default -> com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+ };
+ return requireNonNull(context.getDrawable(iconResIdFromType));
+ }
+
+ private static Drawable getMonochromeIconIfPresent(Drawable icon) {
+ // For created rules, the app should've provided a monochrome Drawable. However, implicit
+ // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
+ // we choose it, if present.
+ if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
+ if (adaptiveIcon.getMonochrome() != null) {
+ // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
+ return new InsetDrawable(adaptiveIcon.getMonochrome(),
+ -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
+ }
+ }
+ return icon;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
new file mode 100644
index 0000000..33d39f0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -0,0 +1,307 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
+import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
+import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.R;
+
+import com.google.common.base.Strings;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Objects;
+
+/**
+ * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
+ *
+ * <p>It also adapts other rule features that we don't want to expose in the UI, such as
+ * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
+ */
+public class ZenMode {
+
+ private static final String TAG = "ZenMode";
+
+ static final String MANUAL_DND_MODE_ID = "manual_dnd";
+
+ // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
+ private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .allowAlarms(true)
+ .allowMedia(true)
+ .allowPriorityChannels(false)
+ .build();
+
+ // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
+ private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .hideAllVisualEffects()
+ .allowPriorityChannels(false)
+ .build();
+
+ public enum Status {
+ ENABLED,
+ ENABLED_AND_ACTIVE,
+ DISABLED_BY_USER,
+ DISABLED_BY_OTHER
+ }
+
+ private final String mId;
+ private final AutomaticZenRule mRule;
+ private final Status mStatus;
+ private final boolean mIsManualDnd;
+
+ /**
+ * Initializes a {@link ZenMode}, mainly based on the information from the
+ * {@link AutomaticZenRule}.
+ *
+ * <p>Some pieces which are not part of the public API (such as whether the mode is currently
+ * active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} --
+ * see {@link #computeStatus}.
+ */
+ public ZenMode(String id, @NonNull AutomaticZenRule rule,
+ @NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
+ this(id, rule, computeStatus(zenRuleExtraData), false);
+ }
+
+ private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
+ if (zenRuleExtraData.enabled) {
+ if (zenRuleExtraData.isAutomaticActive()) {
+ return Status.ENABLED_AND_ACTIVE;
+ } else {
+ return Status.ENABLED;
+ }
+ } else {
+ if (zenRuleExtraData.disabledOrigin == ZenModeConfig.UPDATE_ORIGIN_USER) {
+ return Status.DISABLED_BY_USER;
+ } else {
+ return Status.DISABLED_BY_OTHER; // by APP, SYSTEM, UNKNOWN.
+ }
+ }
+ }
+
+ public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
+ return new ZenMode(MANUAL_DND_MODE_ID, manualRule,
+ isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED, true);
+ }
+
+ private ZenMode(String id, @NonNull AutomaticZenRule rule, Status status, boolean isManualDnd) {
+ mId = id;
+ mRule = rule;
+ mStatus = status;
+ mIsManualDnd = isManualDnd;
+ }
+
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ @NonNull
+ public AutomaticZenRule getRule() {
+ return mRule;
+ }
+
+ @NonNull
+ public String getName() {
+ return Strings.nullToEmpty(mRule.getName());
+ }
+
+ @NonNull
+ public Status getStatus() {
+ return mStatus;
+ }
+
+ @AutomaticZenRule.Type
+ public int getType() {
+ return mRule.getType();
+ }
+
+ @Nullable
+ public String getTriggerDescription() {
+ return mRule.getTriggerDescription();
+ }
+
+ @NonNull
+ public ListenableFuture<Drawable> getIcon(@NonNull Context context,
+ @NonNull ZenIconLoader iconLoader) {
+ if (mIsManualDnd) {
+ return Futures.immediateFuture(requireNonNull(
+ context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+ }
+
+ return iconLoader.getIcon(context, mRule);
+ }
+
+ @NonNull
+ public ZenPolicy getPolicy() {
+ switch (mRule.getInterruptionFilter()) {
+ case INTERRUPTION_FILTER_PRIORITY:
+ case NotificationManager.INTERRUPTION_FILTER_ALL:
+ return requireNonNull(mRule.getZenPolicy());
+
+ case NotificationManager.INTERRUPTION_FILTER_ALARMS:
+ return POLICY_INTERRUPTION_FILTER_ALARMS;
+
+ case NotificationManager.INTERRUPTION_FILTER_NONE:
+ return POLICY_INTERRUPTION_FILTER_NONE;
+
+ case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
+ default:
+ Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
+ + mRule.getInterruptionFilter());
+ return requireNonNull(mRule.getZenPolicy());
+ }
+ }
+
+ /**
+ * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
+ * supplied policy. In some cases this involves conversions, so that the following call
+ * to {@link #getPolicy} might return a different policy from the one supplied here.
+ */
+ @SuppressLint("WrongConstant")
+ public void setPolicy(@NonNull ZenPolicy policy) {
+ ZenPolicy currentPolicy = getPolicy();
+ if (currentPolicy.equals(policy)) {
+ return;
+ }
+
+ if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
+ Log.wtf(TAG, "Able to change policy without filtering being enabled");
+ }
+
+ // If policy is customized from any of the "special" ones, make the rule PRIORITY.
+ if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
+ mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
+ }
+ mRule.setZenPolicy(policy);
+ }
+
+ @NonNull
+ public ZenDeviceEffects getDeviceEffects() {
+ return mRule.getDeviceEffects() != null
+ ? mRule.getDeviceEffects()
+ : new ZenDeviceEffects.Builder().build();
+ }
+
+ public void setCustomModeConditionId(Context context, Uri conditionId) {
+ checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
+ "Trying to change condition of non-system-owned rule %s (to %s)",
+ mRule, conditionId);
+
+ Uri oldCondition = mRule.getConditionId();
+ mRule.setConditionId(conditionId);
+
+ ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
+ if (scheduleInfo != null) {
+ mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
+ mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
+ mRule.setTriggerDescription(
+ getTriggerDescriptionForScheduleTime(context, scheduleInfo));
+ return;
+ }
+
+ ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
+ if (eventInfo != null) {
+ mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
+ mRule.setOwner(ZenModeConfig.getEventConditionProvider());
+ mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
+ return;
+ }
+
+ if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
+ mRule.setType(AutomaticZenRule.TYPE_OTHER);
+ mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
+ mRule.setTriggerDescription("");
+ return;
+ }
+
+ Log.wtf(TAG, String.format(
+ "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
+ + "condition it was!",
+ mRule, oldCondition, conditionId));
+ }
+
+ public boolean canEditName() {
+ return !isManualDnd();
+ }
+
+ public boolean canEditIcon() {
+ return !isManualDnd();
+ }
+
+ public boolean canBeDeleted() {
+ return !isManualDnd();
+ }
+
+ public boolean isManualDnd() {
+ return mIsManualDnd;
+ }
+
+ public boolean isActive() {
+ return mStatus == Status.ENABLED_AND_ACTIVE;
+ }
+
+ public boolean isSystemOwned() {
+ return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return obj instanceof ZenMode other
+ && mId.equals(other.mId)
+ && mRule.equals(other.mRule)
+ && mStatus.equals(other.mStatus)
+ && mIsManualDnd == other.mIsManualDnd;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mRule, mStatus, mIsManualDnd);
+ }
+
+ @Override
+ public String toString() {
+ return mId + " (" + mStatus + ") -> " + mRule;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
new file mode 100644
index 0000000..5529da0
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
@@ -0,0 +1,202 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class used for Settings-NMS interactions related to Mode management.
+ *
+ * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
+ * into the unified {@link ZenMode} format.
+ */
+public class ZenModesBackend {
+
+ private static final String TAG = "ZenModeBackend";
+
+ @Nullable // Until first usage
+ private static ZenModesBackend sInstance;
+
+ private final NotificationManager mNotificationManager;
+
+ private final Context mContext;
+
+ public static ZenModesBackend getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new ZenModesBackend(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ ZenModesBackend(Context context) {
+ mContext = context;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ }
+
+ public List<ZenMode> getModes() {
+ Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
+ ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+
+ ArrayList<ZenMode> modes = new ArrayList<>();
+ modes.add(getManualDndMode(currentConfig));
+
+ for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
+ String ruleId = zenRuleEntry.getKey();
+ ZenModeConfig.ZenRule extraData = currentConfig.automaticRules.get(ruleId);
+ if (extraData != null) {
+ modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(), extraData));
+ } else {
+ Log.w(TAG, "Found AZR " + zenRuleEntry.getValue()
+ + " but no corresponding entry in ZenModeConfig (" + currentConfig
+ + "). Skipping");
+ }
+ }
+
+ // Manual DND first, then alphabetically.
+ modes.sort(Comparator.comparing(ZenMode::isManualDnd).reversed()
+ .thenComparing(ZenMode::getName));
+
+ return modes;
+ }
+
+ @Nullable
+ public ZenMode getMode(String id) {
+ ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+ if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
+ return getManualDndMode(currentConfig);
+ } else {
+ AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
+ ZenModeConfig.ZenRule extraData = currentConfig.automaticRules.get(id);
+ if (rule == null || extraData == null) {
+ return null;
+ }
+ return new ZenMode(id, rule, extraData);
+ }
+ }
+
+ private ZenMode getManualDndMode(ZenModeConfig config) {
+ ZenModeConfig.ZenRule manualRule = config.manualRule;
+ // TODO: b/333682392 - Replace with final strings for name & trigger description
+ AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
+ .setType(manualRule.type)
+ .setZenPolicy(manualRule.zenPolicy)
+ .setDeviceEffects(manualRule.zenDeviceEffects)
+ .setManualInvocationAllowed(manualRule.allowManualInvocation)
+ .setConfigurationActivity(null) // No further settings
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+ .build();
+
+ return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
+ }
+
+ public void updateMode(ZenMode mode) {
+ if (mode.isManualDnd()) {
+ try {
+ NotificationManager.Policy dndPolicy =
+ new ZenModeConfig().toNotificationPolicy(mode.getPolicy());
+ mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
+
+ mNotificationManager.setManualZenRuleDeviceEffects(
+ mode.getRule().getDeviceEffects());
+ } catch (Exception e) {
+ Log.w(TAG, "Error updating manual mode", e);
+ }
+ } else {
+ mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
+ /* fromUser= */ true);
+ }
+ }
+
+ public void activateMode(ZenMode mode, @Nullable Duration forDuration) {
+ if (mode.isManualDnd()) {
+ Uri durationConditionId = null;
+ if (forDuration != null) {
+ durationConditionId = ZenModeConfig.toTimeCondition(mContext,
+ (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id;
+ }
+ mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ durationConditionId, TAG, /* fromUser= */ true);
+
+ } else {
+ if (forDuration != null) {
+ throw new IllegalArgumentException(
+ "Only the manual DND mode can be activated for a specific duration");
+ }
+ mNotificationManager.setAutomaticZenRuleState(mode.getId(),
+ new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE,
+ Condition.SOURCE_USER_ACTION));
+ }
+ }
+
+ public void deactivateMode(ZenMode mode) {
+ if (mode.isManualDnd()) {
+ // When calling with fromUser=true this will not snooze other modes.
+ mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
+ /* fromUser= */ true);
+ } else {
+ // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
+ mNotificationManager.setAutomaticZenRuleState(mode.getId(),
+ new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION));
+ }
+ }
+
+ public void removeMode(ZenMode mode) {
+ if (!mode.canBeDeleted()) {
+ throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
+ }
+ mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
+ }
+
+ /**
+ * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
+ * not have a schedule), this can be later updated by the user in the mode settings page.
+ *
+ * @return the created mode. Only {@code null} if creation failed due to an internal error
+ */
+ @Nullable
+ public ZenMode addCustomMode(String name) {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
+ ZenModeConfig.toCustomManualConditionId())
+ .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName())
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setOwner(ZenModeConfig.getCustomManualConditionProvider())
+ .setManualInvocationAllowed(true)
+ .build();
+
+ String ruleId = mNotificationManager.addAutomaticZenRule(rule);
+ return getMode(ruleId);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
new file mode 100644
index 0000000..20461e3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.ZenPolicy;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenIconLoaderTest {
+
+ private Context mContext;
+ private ZenIconLoader mLoader;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mLoader = new ZenIconLoader(MoreExecutors.newDirectExecutorService());
+ }
+
+ @Test
+ public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
+ AutomaticZenRule systemRule = newRuleBuilder()
+ .setPackage("android")
+ .setIconResId(android.R.drawable.ic_media_play)
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, systemRule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .setIconResId(-123456)
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ private static AutomaticZenRule.Builder newRuleBuilder() {
+ return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().build());
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
new file mode 100644
index 0000000..32cdb98
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.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 com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.net.Uri;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeTest {
+
+ private static final ZenPolicy ZEN_POLICY = new ZenPolicy.Builder().allowAllSounds().build();
+
+ private static final AutomaticZenRule ZEN_RULE =
+ new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setPackage("com.some.driving.thing")
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(ZEN_POLICY)
+ .build();
+
+ @Test
+ public void testBasicMethods() {
+ ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true));
+
+ assertThat(zenMode.getId()).isEqualTo("id");
+ assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
+ assertThat(zenMode.isManualDnd()).isFalse();
+ assertThat(zenMode.canBeDeleted()).isTrue();
+ assertThat(zenMode.isActive()).isTrue();
+
+ ZenMode manualMode = ZenMode.manualDndMode(ZEN_RULE, false);
+ assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
+ assertThat(manualMode.isManualDnd()).isTrue();
+ assertThat(manualMode.canBeDeleted()).isFalse();
+ assertThat(manualMode.isActive()).isFalse();
+ }
+
+ @Test
+ public void constructor_enabledRule_statusEnabled() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(true).build();
+ ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false);
+
+ ZenMode mode = new ZenMode("id", azr, configZenRule);
+ assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.ENABLED);
+ assertThat(mode.isActive()).isFalse();
+ }
+
+ @Test
+ public void constructor_activeRule_statusActive() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(true).build();
+ ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, true);
+
+ ZenMode mode = new ZenMode("id", azr, configZenRule);
+ assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.ENABLED_AND_ACTIVE);
+ assertThat(mode.isActive()).isTrue();
+ }
+
+ @Test
+ public void constructor_disabledRuleByUser_statusDisabledByUser() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(false).build();
+ ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false);
+ configZenRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER;
+
+ ZenMode mode = new ZenMode("id", azr, configZenRule);
+ assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.DISABLED_BY_USER);
+ }
+
+ @Test
+ public void constructor_disabledRuleByOther_statusDisabledByOther() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder(ZEN_RULE).setEnabled(false).build();
+ ZenModeConfig.ZenRule configZenRule = zenConfigRuleFor(azr, false);
+ configZenRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP;
+
+ ZenMode mode = new ZenMode("id", azr, configZenRule);
+ assertThat(mode.getStatus()).isEqualTo(ZenMode.Status.DISABLED_BY_OTHER);
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(ZEN_POLICY)
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+ assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setZenPolicy(ZEN_POLICY) // should be ignored
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+ assertThat(zenMode.getPolicy()).isEqualTo(
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .allowAlarms(true)
+ .allowMedia(true)
+ .allowPriorityChannels(false)
+ .build());
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setZenPolicy(ZEN_POLICY) // should be ignored
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+ assertThat(zenMode.getPolicy()).isEqualTo(
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .hideAllVisualEffects()
+ .allowPriorityChannels(false)
+ .build());
+ }
+
+ @Test
+ public void setPolicy_setsInterruptionFilterPriority() {
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
+
+ zenMode.setPolicy(ZEN_POLICY);
+
+ assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo(
+ INTERRUPTION_FILTER_PRIORITY);
+ assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+ assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY);
+ }
+
+ private static ZenModeConfig.ZenRule zenConfigRuleFor(AutomaticZenRule azr, boolean isActive) {
+ ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
+ zenRule.pkg = azr.getPackageName();
+ zenRule.conditionId = azr.getConditionId();
+ zenRule.enabled = azr.isEnabled();
+ if (isActive) {
+ zenRule.condition = new Condition(azr.getConditionId(), "active", Condition.STATE_TRUE);
+ }
+ return zenRule;
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
new file mode 100644
index 0000000..00c7ae3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_UNKNOWN;
+import static android.service.notification.Condition.STATE_FALSE;
+import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenAdapters.notificationPolicyToZenPolicy;
+import static android.service.notification.ZenPolicy.STATE_ALLOW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AutomaticZenRule;
+import android.app.Flags;
+import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import com.android.settingslib.R;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.time.Duration;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public class ZenModesBackendTest {
+
+ private static final String ZEN_RULE_ID = "rule";
+ private static final AutomaticZenRule ZEN_RULE =
+ new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build();
+
+ private static final AutomaticZenRule MANUAL_DND_RULE =
+ new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build();
+
+ @Mock
+ private NotificationManager mNm;
+
+ private Context mContext;
+ private ZenModesBackend mBackend;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+ SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+ // Helper methods to add active/inactive rule state to a config. Returns a copy.
+ private static ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
+ ZenModeConfig out = base.copy();
+
+ if (active) {
+ out.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ out.manualRule.condition =
+ new Condition(out.manualRule.conditionId, "", STATE_TRUE, SOURCE_UNKNOWN);
+ } else {
+ out.manualRule.zenMode = ZEN_MODE_OFF;
+ out.manualRule.condition =
+ new Condition(out.manualRule.conditionId, "", STATE_FALSE, SOURCE_UNKNOWN);
+ }
+ return out;
+ }
+
+ private static ZenModeConfig configWithRule(ZenModeConfig base, String ruleId,
+ AutomaticZenRule rule, boolean active) {
+ ZenModeConfig out = base.copy();
+ out.automaticRules.put(ruleId, zenConfigRuleForRule(ruleId, rule, active));
+ return out;
+ }
+
+ private static ZenModeConfig.ZenRule zenConfigRuleForRule(String id, AutomaticZenRule azr,
+ boolean active) {
+ // Note that there are many other fields of zenRule, but here we only set the ones
+ // relevant to determining whether or not it is active.
+ ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
+ zenRule.id = id;
+ zenRule.pkg = "package";
+ zenRule.enabled = azr.isEnabled();
+ zenRule.snoozing = false;
+ zenRule.conditionId = azr.getConditionId();
+ zenRule.condition = new Condition(azr.getConditionId(), "",
+ active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION);
+ return zenRule;
+ }
+
+ private static ZenMode newZenMode(String id, AutomaticZenRule azr, boolean active) {
+ return new ZenMode(id, azr, zenConfigRuleForRule(id, azr, active));
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ ShadowApplication shadowApplication = ShadowApplication.getInstance();
+ shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+
+ mContext = RuntimeEnvironment.application;
+ mBackend = new ZenModesBackend(mContext);
+
+ // Default catch-all case with no data. This isn't realistic, but tests below that rely
+ // on the config to get data on rules active will create those individually.
+ when(mNm.getZenModeConfig()).thenReturn(new ZenModeConfig());
+ }
+
+ @Test
+ public void getModes_containsManualDndAndZenRules() {
+ AutomaticZenRule rule2 = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+ .setType(AutomaticZenRule.TYPE_BEDTIME)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+ .build();
+ Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+ Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+ when(mNm.getAutomaticZenRules()).thenReturn(
+ ImmutableMap.of("rule1", ZEN_RULE, "rule2", rule2));
+
+ ZenModeConfig config = new ZenModeConfig();
+ config.applyNotificationPolicy(dndPolicy);
+ config = configWithRule(config, "rule1", ZEN_RULE, false);
+ config = configWithRule(config, "rule2", rule2, false);
+ assertThat(config.manualRule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
+ when(mNm.getZenModeConfig()).thenReturn(config);
+
+ List<ZenMode> modes = mBackend.getModes();
+
+ // all modes exist, but none of them are currently active
+ assertThat(modes).containsExactly(
+ ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title),
+ Uri.EMPTY)
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(notificationPolicyToZenPolicy(dndPolicy))
+ .setManualInvocationAllowed(true)
+ .build(),
+ false),
+ newZenMode("rule2", rule2, false),
+ newZenMode("rule1", ZEN_RULE, false))
+ .inOrder();
+ }
+
+ @Test
+ public void getMode_manualDnd_returnsMode() {
+ Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+ Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+ ZenModeConfig config = new ZenModeConfig();
+ config.applyNotificationPolicy(dndPolicy);
+ when(mNm.getZenModeConfig()).thenReturn(config);
+
+ ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+ assertThat(mode).isEqualTo(
+ ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(notificationPolicyToZenPolicy(dndPolicy))
+ .setManualInvocationAllowed(true)
+ .build(), false));
+ }
+
+ @Test
+ public void getMode_zenRule_returnsMode() {
+ when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(new ZenModeConfig(), ZEN_RULE_ID, ZEN_RULE, false));
+
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+ assertThat(mode).isEqualTo(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+ }
+
+ @Test
+ public void getMode_missingRule_returnsNull() {
+ when(mNm.getAutomaticZenRule(any())).thenReturn(null);
+
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+ assertThat(mode).isNull();
+ verify(mNm).getAutomaticZenRule(eq(ZEN_RULE_ID));
+ }
+
+ @Test
+ public void getMode_manualDnd_returnsCorrectActiveState() {
+ // Set up a base config with an active rule to make sure we're looking at the correct info
+ ZenModeConfig configWithActiveRule = configWithRule(new ZenModeConfig(), ZEN_RULE_ID,
+ ZEN_RULE, true);
+
+ // Equivalent to disallowAllSounds()
+ Policy dndPolicy = new Policy(0, 0, 0);
+ configWithActiveRule.applyNotificationPolicy(dndPolicy);
+ when(mNm.getZenModeConfig()).thenReturn(configWithActiveRule);
+
+ ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+ // By default, manual rule is inactive
+ assertThat(mode).isNotNull();
+ assertThat(mode.isActive()).isFalse();
+
+ // Now the returned config will represent the manual rule being active
+ when(mNm.getZenModeConfig()).thenReturn(configWithManualRule(configWithActiveRule, true));
+ ZenMode activeMode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+ assertThat(activeMode).isNotNull();
+ assertThat(activeMode.isActive()).isTrue();
+ }
+
+ @Test
+ public void getMode_zenRule_returnsCorrectActiveState() {
+ // Set up a base config that has an active manual rule and "rule2", to make sure we're
+ // looking at the correct rule's info.
+ ZenModeConfig configWithActiveRules = configWithRule(
+ configWithManualRule(new ZenModeConfig(), true), // active manual rule
+ "rule2", ZEN_RULE, true); // active rule 2
+
+ when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, false));
+
+ // Round 1: the current config should indicate that the rule is not active
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+ assertThat(mode).isNotNull();
+ assertThat(mode.isActive()).isFalse();
+
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, true));
+ ZenMode activeMode = mBackend.getMode(ZEN_RULE_ID);
+ assertThat(activeMode).isNotNull();
+ assertThat(activeMode.isActive()).isTrue();
+ }
+
+ @Test
+ public void updateMode_manualDnd_setsDeviceEffects() throws Exception {
+ ZenMode manualDnd = ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+ .setZenPolicy(new ZenPolicy())
+ .setDeviceEffects(new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build())
+ .build(), false);
+
+ mBackend.updateMode(manualDnd);
+
+ verify(mNm).setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build());
+ }
+
+ @Test
+ public void updateMode_manualDnd_setsNotificationPolicy() {
+ ZenMode manualDnd = ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build(), false);
+
+ mBackend.updateMode(manualDnd);
+
+ verify(mNm).setNotificationPolicy(eq(new ZenModeConfig().toNotificationPolicy(
+ new ZenPolicy.Builder().allowAllSounds().build())), eq(true));
+ }
+
+ @Test
+ public void updateMode_zenRule_updatesRule() {
+ ZenMode ruleMode = newZenMode("rule", ZEN_RULE, false);
+
+ mBackend.updateMode(ruleMode);
+
+ verify(mNm).updateAutomaticZenRule(eq("rule"), eq(ZEN_RULE), eq(true));
+ }
+
+ @Test
+ public void activateMode_manualDnd_setsZenModeImportant() {
+ mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false), null);
+
+ verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+ any(), eq(true));
+ }
+
+ @Test
+ public void activateMode_manualDndWithDuration_setsZenModeImportantWithCondition() {
+ mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false),
+ Duration.ofMinutes(30));
+
+ verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS),
+ eq(ZenModeConfig.toTimeCondition(mContext, 30, 0, true).id),
+ any(),
+ eq(true));
+ }
+
+ @Test
+ public void activateMode_zenRule_setsRuleStateActive() {
+ mBackend.activateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false), null);
+
+ verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+ eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_TRUE,
+ Condition.SOURCE_USER_ACTION)));
+ }
+
+ @Test
+ public void activateMode_zenRuleWithDuration_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mBackend.activateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false),
+ Duration.ofMinutes(30)));
+ }
+
+ @Test
+ public void deactivateMode_manualDnd_setsZenModeOff() {
+ mBackend.deactivateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, true));
+
+ verify(mNm).setZenMode(eq(ZEN_MODE_OFF), eq(null), any(), eq(true));
+ }
+
+ @Test
+ public void deactivateMode_zenRule_setsRuleStateInactive() {
+ mBackend.deactivateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+ verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+ eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION)));
+ }
+
+ @Test
+ public void removeMode_zenRule_deletesRule() {
+ mBackend.removeMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+ verify(mNm).removeAutomaticZenRule(ZEN_RULE_ID, true);
+ }
+
+ @Test
+ public void removeMode_manualDnd_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mBackend.removeMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false)));
+ }
+}
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index deab818..16dd4e5 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -81,7 +81,7 @@
]
}
],
-
+
"postsubmit": [
{
// Permission indicators
@@ -93,7 +93,7 @@
]
}
],
-
+
// v2/sysui/suite/test-mapping-sysui-screenshot-test
"sysui-screenshot-test": [
{
@@ -131,7 +131,7 @@
]
}
],
-
+
// v2/sysui/suite/test-mapping-sysui-screenshot-test-staged
"sysui-screenshot-test-staged": [
{
@@ -156,5 +156,13 @@
}
]
}
+ ],
+ "sysui-robo-test": [
+ {
+ "name": "SystemUIGoogleRoboRNGTests"
+ },
+ {
+ "name": "SystemUIGoogleRobo2RNGTests"
+ }
]
}
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index f63a896..0861454 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -73,13 +73,10 @@
}
flag {
- name: "redesign_magnifier_window_size"
+ name: "redesign_magnification_window_size"
namespace: "accessibility"
description: "Redesigns the window magnification magnifier sizes provided in the settings panel."
bug: "288056772"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
}
flag {
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 0ccaf18..5ee6008 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1105,3 +1105,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "media_lockscreen_launch_animation"
+ namespace : "systemui"
+ description : "Enable the origin launch animation for UMO when opening on top of lockscreen."
+ bug : "346865769"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt
index 7d82920..1ce51af 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt
@@ -27,6 +27,7 @@
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler
import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection
+import com.android.systemui.communal.ui.compose.section.CommunalPopupSection
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines
import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -44,81 +45,86 @@
private val lockSection: LockSection,
private val bottomAreaSection: BottomAreaSection,
private val ambientStatusBarSection: AmbientStatusBarSection,
+ private val communalPopupSection: CommunalPopupSection,
) {
@Composable
fun SceneScope.Content(modifier: Modifier = Modifier) {
- Layout(
- modifier = modifier.fillMaxSize(),
- content = {
- Box(modifier = Modifier.fillMaxSize()) {
- with(ambientStatusBarSection) {
- AmbientStatusBar(modifier = Modifier.fillMaxWidth())
+ CommunalTouchableSurface(viewModel = viewModel, modifier = modifier) {
+ Layout(
+ modifier = Modifier.fillMaxSize(),
+ content = {
+ Box(modifier = Modifier.fillMaxSize()) {
+ with(communalPopupSection) { Popup() }
+ with(ambientStatusBarSection) {
+ AmbientStatusBar(modifier = Modifier.fillMaxWidth())
+ }
+ CommunalHub(
+ viewModel = viewModel,
+ interactionHandler = interactionHandler,
+ dialogFactory = dialogFactory,
+ modifier = Modifier.element(Communal.Elements.Grid)
+ )
}
- CommunalHub(
- viewModel = viewModel,
- interactionHandler = interactionHandler,
- dialogFactory = dialogFactory,
- modifier = Modifier.element(Communal.Elements.Grid)
+ with(lockSection) {
+ LockIcon(
+ overrideColor = LocalAndroidColorScheme.current.onPrimaryContainer,
+ modifier = Modifier.element(Communal.Elements.LockIcon)
+ )
+ }
+ with(bottomAreaSection) {
+ IndicationArea(
+ Modifier.element(Communal.Elements.IndicationArea).fillMaxWidth()
+ )
+ }
+ }
+ ) { measurables, constraints ->
+ val communalGridMeasurable = measurables[0]
+ val lockIconMeasurable = measurables[1]
+ val bottomAreaMeasurable = measurables[2]
+
+ val noMinConstraints =
+ constraints.copy(
+ minWidth = 0,
+ minHeight = 0,
+ )
+
+ val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
+ val lockIconBounds =
+ IntRect(
+ left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
+ top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
+ right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
+ bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
+ )
+
+ val bottomAreaPlaceable =
+ bottomAreaMeasurable.measure(
+ noMinConstraints.copy(
+ maxHeight =
+ (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
+ )
+ )
+
+ val communalGridPlaceable =
+ communalGridMeasurable.measure(
+ noMinConstraints.copy(maxHeight = lockIconBounds.top)
+ )
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ communalGridPlaceable.place(
+ x = 0,
+ y = 0,
+ )
+ lockIconPlaceable.place(
+ x = lockIconBounds.left,
+ y = lockIconBounds.top,
+ )
+ bottomAreaPlaceable.place(
+ x = 0,
+ y = constraints.maxHeight - bottomAreaPlaceable.height,
)
}
- with(lockSection) {
- LockIcon(
- overrideColor = LocalAndroidColorScheme.current.onPrimaryContainer,
- modifier = Modifier.element(Communal.Elements.LockIcon)
- )
- }
- with(bottomAreaSection) {
- IndicationArea(
- Modifier.element(Communal.Elements.IndicationArea).fillMaxWidth()
- )
- }
- }
- ) { measurables, constraints ->
- val communalGridMeasurable = measurables[0]
- val lockIconMeasurable = measurables[1]
- val bottomAreaMeasurable = measurables[2]
-
- val noMinConstraints =
- constraints.copy(
- minWidth = 0,
- minHeight = 0,
- )
-
- val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
- val lockIconBounds =
- IntRect(
- left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
- top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
- right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
- bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
- )
-
- val bottomAreaPlaceable =
- bottomAreaMeasurable.measure(
- noMinConstraints.copy(
- maxHeight = (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
- )
- )
-
- val communalGridPlaceable =
- communalGridMeasurable.measure(
- noMinConstraints.copy(maxHeight = lockIconBounds.top)
- )
-
- layout(constraints.maxWidth, constraints.maxHeight) {
- communalGridPlaceable.place(
- x = 0,
- y = 0,
- )
- lockIconPlaceable.place(
- x = lockIconBounds.left,
- y = lockIconBounds.top,
- )
- bottomAreaPlaceable.place(
- x = 0,
- y = constraints.maxHeight - bottomAreaPlaceable.height,
- )
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 7062489..5efa5d5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -25,7 +25,6 @@
import android.widget.RemoteViews
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
@@ -49,6 +48,7 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -70,7 +70,6 @@
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Edit
-import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material.icons.outlined.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
@@ -102,10 +101,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
-import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.key.onPreviewKeyEvent
-import androidx.compose.ui.input.pointer.motionEventSpy
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
@@ -125,14 +121,11 @@
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import androidx.compose.ui.viewinterop.AndroidView
-import androidx.compose.ui.window.Popup
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.layout.WindowMetricsCalculator
import com.android.compose.animation.Easings.Emphasized
@@ -151,7 +144,6 @@
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
-import com.android.systemui.communal.ui.viewmodel.PopupType
import com.android.systemui.communal.widgets.SmartspaceAppWidgetHostView
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.res.R
@@ -171,7 +163,6 @@
) {
val communalContent by
viewModel.communalContent.collectAsStateWithLifecycle(initialValue = emptyList())
- val currentPopup by viewModel.currentPopup.collectAsStateWithLifecycle(initialValue = null)
var removeButtonCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
var toolbarSize: IntSize? by remember { mutableStateOf(null) }
var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
@@ -213,43 +204,29 @@
}
.thenIf(!viewModel.isEditMode && !isEmptyState) {
Modifier.pointerInput(
- gridState,
- contentOffset,
- communalContent,
- gridCoordinates
- ) {
- detectLongPressGesture { offset ->
- // Deduct both grid offset relative to its container and content
- // offset.
- val adjustedOffset =
- gridCoordinates?.let {
- offset - it.positionInWindow() - contentOffset
- }
- val index =
- adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
- // Display the button only when the gesture initiates from widgets,
- // the CTA tile, or an empty area on the screen. UMO/smartspace have
- // their own long-press handlers. To prevent user confusion, we
- // should
- // not display this button.
- if (
- index == null ||
- communalContent[index].isWidgetContent() ||
- communalContent[index] is
- CommunalContentModel.CtaTileInViewMode
- ) {
- viewModel.onShowCustomizeWidgetButton()
+ gridState,
+ contentOffset,
+ communalContent,
+ gridCoordinates
+ ) {
+ detectLongPressGesture { offset ->
+ // Deduct both grid offset relative to its container and content
+ // offset.
+ val adjustedOffset =
+ gridCoordinates?.let {
+ offset - it.positionInWindow() - contentOffset
}
- val key =
- index?.let { keyAtIndexIfEditable(communalContent, index) }
+ val index = adjustedOffset?.let { firstIndexAtOffset(gridState, it) }
+ val key = index?.let { keyAtIndexIfEditable(communalContent, index) }
+ // Handle long-click on widgets and set the selected index
+ // correctly. We only handle widgets here because long click on
+ // empty spaces is handled by CommunalPopupSection.
+ if (key != null) {
+ viewModel.onLongClick()
viewModel.setSelectedKey(key)
}
}
- .onPreviewKeyEvent {
- onKeyEvent(viewModel)
- false
- }
- .motionEventSpy { onMotionEvent(viewModel) }
+ }
},
) {
AccessibilityContainer(viewModel) {
@@ -342,22 +319,6 @@
)
}
}
- if (currentPopup == PopupType.CtaTile) {
- PopupOnDismissCtaTile(viewModel::onHidePopup)
- }
-
- AnimatedVisibility(
- visible = currentPopup == PopupType.CustomizeWidgetButton,
- modifier = Modifier.fillMaxSize()
- ) {
- ButtonToEditWidgets(
- onClick = {
- viewModel.onHidePopup()
- viewModel.onOpenWidgetEditor(selectedKey.value)
- },
- onHide = { viewModel.onHidePopup() }
- )
- }
if (viewModel is CommunalViewModel && dialogFactory != null) {
val isEnableWidgetDialogShowing by
@@ -413,14 +374,6 @@
}
}
-private fun onKeyEvent(viewModel: BaseCommunalViewModel) {
- viewModel.signalUserInteraction()
-}
-
-private fun onMotionEvent(viewModel: BaseCommunalViewModel) {
- viewModel.signalUserInteraction()
-}
-
@Composable
private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) {
val colors = LocalAndroidColorScheme.current
@@ -821,107 +774,6 @@
}
@Composable
-private fun AnimatedVisibilityScope.ButtonToEditWidgets(
- onClick: () -> Unit,
- onHide: () -> Unit,
-) {
- Popup(
- alignment = Alignment.TopCenter,
- offset = IntOffset(0, 40),
- onDismissRequest = onHide,
- ) {
- val colors = LocalAndroidColorScheme.current
- Button(
- modifier =
- Modifier.height(56.dp)
- .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) }
- .animateEnterExit(
- enter =
- fadeIn(
- initialAlpha = 0f,
- animationSpec = tween(durationMillis = 83, easing = LinearEasing)
- ),
- exit =
- fadeOut(
- animationSpec =
- tween(
- durationMillis = 83,
- delayMillis = 167,
- easing = LinearEasing
- )
- )
- )
- .background(colors.secondary, RoundedCornerShape(50.dp)),
- onClick = onClick,
- ) {
- Row(
- modifier =
- Modifier.animateEnterExit(
- enter =
- fadeIn(
- animationSpec =
- tween(
- durationMillis = 167,
- delayMillis = 83,
- easing = LinearEasing
- )
- ),
- exit =
- fadeOut(
- animationSpec = tween(durationMillis = 167, easing = LinearEasing)
- )
- )
- ) {
- Icon(
- imageVector = Icons.Outlined.Widgets,
- contentDescription = stringResource(R.string.button_to_configure_widgets_text),
- tint = colors.onSecondary,
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.size(8.dp))
- Text(
- text = stringResource(R.string.button_to_configure_widgets_text),
- style = MaterialTheme.typography.titleSmall,
- color = colors.onSecondary
- )
- }
- }
- }
-}
-
-@Composable
-private fun PopupOnDismissCtaTile(onHidePopup: () -> Unit) {
- Popup(
- alignment = Alignment.TopCenter,
- offset = IntOffset(0, 40),
- onDismissRequest = onHidePopup
- ) {
- val colors = LocalAndroidColorScheme.current
- Row(
- modifier =
- Modifier.height(56.dp)
- .background(colors.secondary, RoundedCornerShape(50.dp))
- .padding(16.dp),
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Icon(
- imageVector = Icons.Outlined.TouchApp,
- contentDescription = stringResource(R.string.popup_on_dismiss_cta_tile_text),
- tint = colors.onSecondary,
- modifier = Modifier.size(20.dp)
- )
- Spacer(modifier = Modifier.size(8.dp))
- Text(
- text = stringResource(R.string.popup_on_dismiss_cta_tile_text),
- style = MaterialTheme.typography.titleSmall,
- color = colors.onSecondary,
- )
- }
- }
-}
-
-@Composable
private fun filledButtonColors(): ButtonColors {
val colors = LocalAndroidColorScheme.current
return ButtonDefaults.buttonColors(
@@ -994,7 +846,8 @@
shape = RoundedCornerShape(68.dp, 34.dp, 68.dp, 34.dp)
) {
Column(
- modifier = Modifier.fillMaxSize().padding(vertical = 38.dp, horizontal = 70.dp),
+ modifier = Modifier.fillMaxSize().padding(vertical = 32.dp, horizontal = 50.dp),
+ verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
@@ -1005,41 +858,43 @@
Spacer(modifier = Modifier.size(6.dp))
Text(
text = stringResource(R.string.cta_label_to_edit_widget),
- style = MaterialTheme.typography.titleMedium,
- textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ fontSize = nonScalableTextSize(22.dp),
+ lineHeight = nonScalableTextSize(28.dp),
)
- Spacer(modifier = Modifier.size(20.dp))
+ Spacer(modifier = Modifier.size(16.dp))
Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxWidth().height(56.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
) {
OutlinedButton(
+ modifier = Modifier.fillMaxHeight(),
colors =
ButtonDefaults.buttonColors(
contentColor = colors.onPrimary,
),
border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
- contentPadding = Dimensions.ButtonPadding,
+ contentPadding = PaddingValues(26.dp, 8.dp),
onClick = viewModel::onDismissCtaTile,
) {
Text(
text = stringResource(R.string.cta_tile_button_to_dismiss),
- fontSize = 12.sp,
+ fontSize = nonScalableTextSize(14.dp),
)
}
- Spacer(modifier = Modifier.size(14.dp))
Button(
+ modifier = Modifier.fillMaxHeight(),
colors =
ButtonDefaults.buttonColors(
containerColor = colors.primaryContainer,
contentColor = colors.onPrimaryContainer,
),
- contentPadding = Dimensions.ButtonPadding,
+ contentPadding = PaddingValues(26.dp, 8.dp),
onClick = viewModel::onOpenWidgetEditor
) {
Text(
text = stringResource(R.string.cta_tile_button_to_open_widget_editor),
- fontSize = 12.sp,
+ fontSize = nonScalableTextSize(14.dp),
)
}
}
@@ -1352,6 +1207,13 @@
}
/**
+ * Text size converted from dp value to the equivalent sp value using the current screen density,
+ * ensuring it does not scale with the font size setting.
+ */
+@Composable
+private fun nonScalableTextSize(sizeInDp: Dp) = with(LocalDensity.current) { sizeInDp.toSp() }
+
+/**
* Returns the `contentPadding` of the grid. Use the vertical padding to push the grid content area
* below the toolbar and let the grid take the max size. This ensures the item can be dragged
* outside the grid over the toolbar, without part of it getting clipped by the container.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalTouchableSurface.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalTouchableSurface.kt
new file mode 100644
index 0000000..3707a87
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalTouchableSurface.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.ui.compose
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.onPreviewKeyEvent
+import androidx.compose.ui.input.pointer.motionEventSpy
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+
+@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
+@Composable
+fun CommunalTouchableSurface(
+ viewModel: CommunalViewModel,
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit,
+) {
+
+ val interactionSource = remember { MutableInteractionSource() }
+
+ Box(
+ modifier =
+ modifier
+ .combinedClickable(
+ onLongClick = viewModel::onLongClick,
+ onClick = viewModel::onClick,
+ interactionSource = interactionSource,
+ indication = null,
+ )
+ .onPreviewKeyEvent {
+ viewModel.signalUserInteraction()
+ false
+ }
+ .motionEventSpy { viewModel.signalUserInteraction() }
+ ) {
+ content()
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
new file mode 100644
index 0000000..1ea73e1
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalPopupSection.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.ui.compose.section
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.TouchApp
+import androidx.compose.material.icons.outlined.Widgets
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.PopupType
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class CommunalPopupSection
+@Inject
+constructor(
+ private val viewModel: CommunalViewModel,
+) {
+
+ @Composable
+ fun Popup() {
+ val currentPopup by viewModel.currentPopup.collectAsStateWithLifecycle(initialValue = null)
+
+ if (currentPopup == PopupType.CtaTile) {
+ PopupOnDismissCtaTile(viewModel::onHidePopup)
+ }
+
+ AnimatedVisibility(
+ visible = currentPopup == PopupType.CustomizeWidgetButton,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ ButtonToEditWidgets(
+ onClick = {
+ viewModel.onHidePopup()
+ viewModel.onOpenWidgetEditor()
+ },
+ onDismissRequest = {
+ viewModel.onHidePopup()
+ viewModel.setSelectedKey(null)
+ }
+ )
+ }
+ }
+
+ @Composable
+ private fun AnimatedVisibilityScope.ButtonToEditWidgets(
+ onClick: () -> Unit,
+ onDismissRequest: () -> Unit,
+ ) {
+ Popup(
+ alignment = Alignment.TopCenter,
+ offset = IntOffset(0, 40),
+ onDismissRequest = onDismissRequest,
+ ) {
+ val colors = LocalAndroidColorScheme.current
+ Button(
+ modifier =
+ Modifier.height(56.dp)
+ .graphicsLayer { transformOrigin = TransformOrigin(0f, 0f) }
+ .animateEnterExit(
+ enter =
+ fadeIn(
+ initialAlpha = 0f,
+ animationSpec =
+ tween(durationMillis = 83, easing = LinearEasing)
+ ),
+ exit =
+ fadeOut(
+ animationSpec =
+ tween(
+ durationMillis = 83,
+ delayMillis = 167,
+ easing = LinearEasing
+ )
+ )
+ )
+ .background(colors.secondary, RoundedCornerShape(50.dp)),
+ onClick = onClick,
+ ) {
+ Row(
+ modifier =
+ Modifier.animateEnterExit(
+ enter =
+ fadeIn(
+ animationSpec =
+ tween(
+ durationMillis = 167,
+ delayMillis = 83,
+ easing = LinearEasing
+ )
+ ),
+ exit =
+ fadeOut(
+ animationSpec =
+ tween(durationMillis = 167, easing = LinearEasing)
+ )
+ )
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Widgets,
+ contentDescription =
+ stringResource(R.string.button_to_configure_widgets_text),
+ tint = colors.onSecondary,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(
+ text = stringResource(R.string.button_to_configure_widgets_text),
+ style = MaterialTheme.typography.titleSmall,
+ color = colors.onSecondary
+ )
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun PopupOnDismissCtaTile(onDismissRequest: () -> Unit) {
+ Popup(
+ alignment = Alignment.TopCenter,
+ offset = IntOffset(0, 40),
+ onDismissRequest = onDismissRequest
+ ) {
+ val colors = LocalAndroidColorScheme.current
+ Row(
+ modifier =
+ Modifier.height(56.dp)
+ .background(colors.secondary, RoundedCornerShape(50.dp))
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.TouchApp,
+ contentDescription = stringResource(R.string.popup_on_dismiss_cta_tile_text),
+ tint = colors.onSecondary,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(
+ text = stringResource(R.string.popup_on_dismiss_cta_tile_text),
+ style = MaterialTheme.typography.titleSmall,
+ color = colors.onSecondary,
+ )
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
index 899b256..db98bc8f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -96,6 +96,12 @@
shadeMode = ShadeMode.Dual,
modifier = Modifier.fillMaxWidth(),
)
+
+ // Communicates the bottom position of the drawable area within the shade to NSSL.
+ NotificationStackCutoffGuideline(
+ stackScrollView = stackScrollView.get(),
+ viewModel = notificationsPlaceholderViewModel,
+ )
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
index 4914aea..c066ae5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -154,6 +154,7 @@
viewModel = viewModel.tileGridViewModel,
modifier =
Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
+ viewModel.editModeViewModel::startEditing,
)
Button(
onClick = { viewModel.editModeViewModel.startEditing() },
@@ -168,7 +169,7 @@
object Dimensions {
val Padding = 16.dp
val BrightnessSliderHeight = 64.dp
- val GridMaxHeight = 400.dp
+ val GridMaxHeight = 800.dp
}
object Transitions {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index dbf6cd3..e433d32 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -50,30 +50,22 @@
from(Scenes.Gone, to = Scenes.NotificationsShade, key = OpenBottomShade) {
goneToNotificationsShadeTransition(Edge.Bottom)
}
- from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
- from(
- Scenes.Gone,
- to = Scenes.Shade,
- key = ToSplitShade,
- ) {
- goneToSplitShadeTransition()
+ from(Scenes.Gone, to = Scenes.QuickSettingsShade) {
+ goneToQuickSettingsShadeTransition(Edge.Top)
}
- from(
- Scenes.Gone,
- to = Scenes.Shade,
- key = SlightlyFasterShadeCollapse,
- ) {
+ from(Scenes.Gone, to = Scenes.QuickSettingsShade, key = OpenBottomShade) {
+ goneToQuickSettingsShadeTransition(Edge.Bottom)
+ }
+ from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
+ from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
+ from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
goneToShadeTransition(durationScale = 0.9)
}
from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
- from(
- Scenes.Gone,
- to = Scenes.QuickSettings,
- key = SlightlyFasterShadeCollapse,
- ) {
+ from(Scenes.Gone, to = Scenes.QuickSettings, key = SlightlyFasterShadeCollapse) {
goneToQuickSettingsTransition(durationScale = 0.9)
}
- from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
+
from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
from(Scenes.Lockscreen, to = Scenes.NotificationsShade) {
@@ -83,18 +75,10 @@
lockscreenToQuickSettingsShadeTransition()
}
from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
- from(
- Scenes.Lockscreen,
- to = Scenes.Shade,
- key = ToSplitShade,
- ) {
+ from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) {
lockscreenToSplitShadeTransition()
}
- from(
- Scenes.Lockscreen,
- to = Scenes.Shade,
- key = SlightlyFasterShadeCollapse,
- ) {
+ from(Scenes.Lockscreen, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
lockscreenToShadeTransition(durationScale = 0.9)
}
from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
index 225ca4e..8a03e29 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
@@ -16,10 +16,12 @@
package com.android.systemui.scene.ui.composable.transitions
+import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
fun TransitionBuilder.goneToQuickSettingsShadeTransition(
+ edge: Edge = Edge.Top,
durationScale: Double = 1.0,
) {
- toQuickSettingsShadeTransition(durationScale)
+ toQuickSettingsShadeTransition(edge, durationScale)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
index ce24f5e..19aa3a7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
@@ -16,10 +16,11 @@
package com.android.systemui.scene.ui.composable.transitions
+import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
fun TransitionBuilder.lockscreenToQuickSettingsShadeTransition(
durationScale: Double = 1.0,
) {
- toQuickSettingsShadeTransition(durationScale)
+ toQuickSettingsShadeTransition(Edge.Top, durationScale)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
index ec2f14f..9d13647 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -19,17 +19,15 @@
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.Shade
import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.toQuickSettingsShadeTransition(
+ edge: Edge = Edge.Top,
durationScale: Double = 1.0,
) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
@@ -38,17 +36,9 @@
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
)
- distance =
- object : UserActionDistance {
- override fun UserActionDistanceScope.absoluteDistance(
- fromSceneSize: IntSize,
- orientation: Orientation,
- ): Float {
- return fromSceneSize.height.toFloat() * 2 / 3f
- }
- }
+ distance = UserActionDistance { fromSceneSize, _ -> fromSceneSize.height.toFloat() * 2 / 3f }
- translate(OverlayShade.Elements.Panel, Edge.Top)
+ translate(OverlayShade.Elements.Panel, edge)
fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
}
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/BouncerViewModelTest.kt
index a5acf72..ccddc9c 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/BouncerViewModelTest.kt
@@ -38,7 +38,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
+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
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index cf14547..fbe2c2e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -451,6 +451,24 @@
}
}
+ @Test
+ fun transitionFromDozingToGlanceableHub_forcesCommunal() =
+ with(kosmos) {
+ testScope.runTest {
+ val scene by collectLastValue(communalSceneInteractor.currentScene)
+ communalSceneInteractor.changeScene(CommunalScenes.Blank)
+ assertThat(scene).isEqualTo(CommunalScenes.Blank)
+
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.GLANCEABLE_HUB,
+ testScope = this
+ )
+
+ assertThat(scene).isEqualTo(CommunalScenes.Communal)
+ }
+ }
+
private fun TestScope.updateDocked(docked: Boolean) =
with(kosmos) {
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 7a5f81c..51991de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -75,6 +75,7 @@
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
+import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
@@ -91,6 +92,7 @@
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
@@ -154,13 +156,14 @@
context.resources,
kosmos.keyguardTransitionInteractor,
kosmos.keyguardInteractor,
+ mock<KeyguardIndicationController>(),
kosmos.communalSceneInteractor,
kosmos.communalInteractor,
kosmos.communalSettingsInteractor,
kosmos.communalTutorialInteractor,
kosmos.shadeInteractor,
mediaHost,
- logcatLogBuffer("CommunalViewModelTest"),
+ logcatLogBuffer("CommunalViewModelTest")
)
}
@@ -358,7 +361,7 @@
val currentPopup by collectLastValue(underTest.currentPopup)
assertThat(currentPopup).isNull()
- underTest.onShowCustomizeWidgetButton()
+ underTest.onLongClick()
assertThat(currentPopup).isEqualTo(PopupType.CustomizeWidgetButton)
advanceTimeBy(POPUP_AUTO_HIDE_TIMEOUT_MS)
assertThat(currentPopup).isNull()
@@ -370,7 +373,7 @@
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
val currentPopup by collectLastValue(underTest.currentPopup)
- underTest.onShowCustomizeWidgetButton()
+ underTest.onLongClick()
assertThat(currentPopup).isEqualTo(PopupType.CustomizeWidgetButton)
underTest.onHidePopup()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
index 5f0f24d..2d12150 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -30,6 +30,7 @@
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -61,7 +62,8 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- testScope = TestScope()
+ val testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
userRepository = FakeUserRepository()
userRepository.setUserInfos(users)
val logger =
@@ -69,7 +71,13 @@
LogBuffer("TestBuffer", 1, mock(LogcatEchoTracker::class.java), false)
)
underTest =
- TrustRepositoryImpl(testScope.backgroundScope, userRepository, trustManager, logger)
+ TrustRepositoryImpl(
+ testScope.backgroundScope,
+ testDispatcher,
+ userRepository,
+ trustManager,
+ logger,
+ )
}
fun TestScope.init() {
@@ -275,4 +283,11 @@
userRepository.setSelectedUserInfo(users[1])
assertThat(trustUsuallyManaged).isFalse()
}
+
+ @Test
+ fun reportKeyguardShowingChanged() =
+ testScope.runTest {
+ underTest.reportKeyguardShowingChanged()
+ verify(trustManager).reportKeyguardShowingChanged()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 612f2e7..0792a50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -34,12 +34,14 @@
import android.os.PowerManager
import android.platform.test.annotations.EnableFlags
+import android.service.dream.dreamManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -64,8 +66,10 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
+import org.mockito.kotlin.whenever
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -120,6 +124,66 @@
@Test
@EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testTransitionToLockscreen_onWakeup_canDream_glanceableHubAvailable() =
+ testScope.runTest {
+ whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ powerInteractor.setAwakeForTest()
+ runCurrent()
+
+ // If dreaming is possible and communal is available, then we should transition to
+ // GLANCEABLE_HUB when waking up.
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.GLANCEABLE_HUB,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testTransitionToLockscreen_onWakeup_canNotDream_glanceableHubAvailable() =
+ testScope.runTest {
+ whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
+ kosmos.setCommunalAvailable(true)
+ runCurrent()
+
+ powerInteractor.setAwakeForTest()
+ runCurrent()
+
+ // If dreaming is NOT possible but communal is available, then we should transition to
+ // LOCKSCREEN when waking up.
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testTransitionToLockscreen_onWakeup_canNDream_glanceableHubNotAvailable() =
+ testScope.runTest {
+ whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+ kosmos.setCommunalAvailable(false)
+ runCurrent()
+
+ powerInteractor.setAwakeForTest()
+ runCurrent()
+
+ // If dreaming is possible but communal is NOT available, then we should transition to
+ // LOCKSCREEN when waking up.
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() =
testScope.runTest {
kosmos.fakeCommunalSceneRepository.setTransitionState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
index 3777e40..6f74ed3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -16,98 +16,108 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags as AConfigFlags
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardBottomAreaInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.BurnInModel
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class KeyguardIndicationAreaViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class KeyguardIndicationAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
- @Mock private lateinit var shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel
-
- @Mock private lateinit var burnInInteractor: BurnInInteractor
- private val burnInFlow = MutableStateFlow(BurnInModel())
-
- private lateinit var bottomAreaInteractor: KeyguardBottomAreaInteractor
+ private val bottomAreaInteractor = kosmos.keyguardBottomAreaInteractor
private lateinit var underTest: KeyguardIndicationAreaViewModel
- private lateinit var repository: FakeKeyguardRepository
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val communalSceneRepository = kosmos.fakeCommunalSceneRepository
private val startButtonFlow =
- MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+ MutableStateFlow(
KeyguardQuickAffordanceViewModel(
slotId = KeyguardQuickAffordancePosition.BOTTOM_START.toSlotId()
)
)
private val endButtonFlow =
- MutableStateFlow<KeyguardQuickAffordanceViewModel>(
+ MutableStateFlow(
KeyguardQuickAffordanceViewModel(
slotId = KeyguardQuickAffordancePosition.BOTTOM_END.toSlotId()
)
)
- private val alphaFlow = MutableStateFlow<Float>(1f)
+ private val alphaFlow = MutableStateFlow(1f)
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
@Before
fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
- mSetFlagsRule.disableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-
- whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
- .thenReturn(RETURNED_BURN_IN_OFFSET)
- whenever(burnInInteractor.burnIn(anyInt(), anyInt())).thenReturn(burnInFlow)
-
- val withDeps = KeyguardInteractorFactory.create()
- val keyguardInteractor = withDeps.keyguardInteractor
- repository = withDeps.repository
-
- val bottomAreaViewModel: KeyguardBottomAreaViewModel = mock()
- whenever(bottomAreaViewModel.startButton).thenReturn(startButtonFlow)
- whenever(bottomAreaViewModel.endButton).thenReturn(endButtonFlow)
- whenever(bottomAreaViewModel.alpha).thenReturn(alphaFlow)
- bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository)
+ val bottomAreaViewModel =
+ mock<KeyguardBottomAreaViewModel> {
+ on { startButton } doReturn startButtonFlow
+ on { endButton } doReturn endButtonFlow
+ on { alpha } doReturn alphaFlow
+ }
+ val burnInInteractor =
+ mock<BurnInInteractor> {
+ on { burnIn(anyInt(), anyInt()) } doReturn flowOf(BurnInModel())
+ }
+ val burnInHelperWrapper =
+ mock<BurnInHelperWrapper> {
+ on { burnInOffset(anyInt(), any()) } doReturn RETURNED_BURN_IN_OFFSET
+ }
+ val shortcutsCombinedViewModel =
+ mock<KeyguardQuickAffordancesCombinedViewModel> {
+ on { startButton } doReturn startButtonFlow
+ on { endButton } doReturn endButtonFlow
+ }
underTest =
KeyguardIndicationAreaViewModel(
- keyguardInteractor = keyguardInteractor,
+ keyguardInteractor = kosmos.keyguardInteractor,
bottomAreaInteractor = bottomAreaInteractor,
keyguardBottomAreaViewModel = bottomAreaViewModel,
burnInHelperWrapper = burnInHelperWrapper,
burnInInteractor = burnInInteractor,
shortcutsCombinedViewModel = shortcutsCombinedViewModel,
- configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
+ configurationInteractor = kosmos.configurationInteractor,
keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
- backgroundCoroutineContext = kosmos.testDispatcher,
+ backgroundDispatcher = kosmos.testDispatcher,
+ communalSceneInteractor = kosmos.communalSceneInteractor,
mainDispatcher = kosmos.testDispatcher
)
}
@@ -115,77 +125,120 @@
@Test
fun alpha() =
testScope.runTest {
- val value = collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha)
- assertThat(value()).isEqualTo(1f)
+ assertThat(alpha).isEqualTo(1f)
alphaFlow.value = 0.1f
- assertThat(value()).isEqualTo(0.1f)
+ assertThat(alpha).isEqualTo(0.1f)
alphaFlow.value = 0.5f
- assertThat(value()).isEqualTo(0.5f)
+ assertThat(alpha).isEqualTo(0.5f)
alphaFlow.value = 0.2f
- assertThat(value()).isEqualTo(0.2f)
+ assertThat(alpha).isEqualTo(0.2f)
alphaFlow.value = 0f
- assertThat(value()).isEqualTo(0f)
+ assertThat(alpha).isEqualTo(0f)
}
@Test
+ @DisableFlags(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
fun isIndicationAreaPadded() =
testScope.runTest {
- repository.setKeyguardShowing(true)
- val value = collectLastValue(underTest.isIndicationAreaPadded)
+ keyguardRepository.setKeyguardShowing(true)
+ val isIndicationAreaPadded by collectLastValue(underTest.isIndicationAreaPadded)
- assertThat(value()).isFalse()
+ assertThat(isIndicationAreaPadded).isFalse()
startButtonFlow.value = startButtonFlow.value.copy(isVisible = true)
- assertThat(value()).isTrue()
+ assertThat(isIndicationAreaPadded).isTrue()
endButtonFlow.value = endButtonFlow.value.copy(isVisible = true)
- assertThat(value()).isTrue()
+ assertThat(isIndicationAreaPadded).isTrue()
startButtonFlow.value = startButtonFlow.value.copy(isVisible = false)
- assertThat(value()).isTrue()
+ assertThat(isIndicationAreaPadded).isTrue()
endButtonFlow.value = endButtonFlow.value.copy(isVisible = false)
- assertThat(value()).isFalse()
+ assertThat(isIndicationAreaPadded).isFalse()
}
@Test
+ @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
fun indicationAreaTranslationX() =
testScope.runTest {
- val value = collectLastValue(underTest.indicationAreaTranslationX)
+ val translationX by collectLastValue(underTest.indicationAreaTranslationX)
- assertThat(value()).isEqualTo(0f)
+ assertThat(translationX).isEqualTo(0f)
bottomAreaInteractor.setClockPosition(100, 100)
- assertThat(value()).isEqualTo(100f)
+ assertThat(translationX).isEqualTo(100f)
bottomAreaInteractor.setClockPosition(200, 100)
- assertThat(value()).isEqualTo(200f)
+ assertThat(translationX).isEqualTo(200f)
bottomAreaInteractor.setClockPosition(200, 200)
- assertThat(value()).isEqualTo(200f)
+ assertThat(translationX).isEqualTo(200f)
bottomAreaInteractor.setClockPosition(300, 100)
- assertThat(value()).isEqualTo(300f)
+ assertThat(translationX).isEqualTo(300f)
}
@Test
+ @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
fun indicationAreaTranslationY() =
testScope.runTest {
- val value =
+ val translationY by
collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET))
// Negative 0 - apparently there's a difference in floating point arithmetic - FML
- assertThat(value()).isEqualTo(-0f)
+ assertThat(translationY).isEqualTo(-0f)
val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f)
- assertThat(value()).isEqualTo(expected1)
+ assertThat(translationY).isEqualTo(expected1)
val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f)
- assertThat(value()).isEqualTo(expected2)
+ assertThat(translationY).isEqualTo(expected2)
val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f)
- assertThat(value()).isEqualTo(expected3)
+ assertThat(translationY).isEqualTo(expected3)
val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f)
- assertThat(value()).isEqualTo(expected4)
+ assertThat(translationY).isEqualTo(expected4)
+ }
+
+ @Test
+ fun visibilityWhenCommunalNotShowing() =
+ testScope.runTest {
+ keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ val visible by collectLastValue(underTest.visible)
+
+ assertThat(visible).isTrue()
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ assertThat(visible).isFalse()
+ }
+
+ @Test
+ fun visibilityWhenCommunalShowing() =
+ testScope.runTest {
+ keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+ communalSceneRepository.setTransitionState(
+ flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
+ )
+
+ val visible by collectLastValue(underTest.visible)
+
+ assertThat(visible).isTrue()
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ assertThat(visible).isTrue()
+
+ communalSceneRepository.setTransitionState(
+ flowOf(ObservableTransitionState.Idle(CommunalScenes.Blank))
+ )
+ assertThat(visible).isFalse()
}
private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
- repository.setDozeAmount(dozeAmount)
+ keyguardRepository.setDozeAmount(dozeAmount)
return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
}
companion object {
private const val DEFAULT_BURN_IN_OFFSET = 5
private const val RETURNED_BURN_IN_OFFSET = 3
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
+ FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
+ )
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
index cb4e2d3..16c7090 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
@@ -28,12 +28,14 @@
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -56,7 +58,7 @@
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
- private val underTest = kosmos.notificationsShadeSceneViewModel
+ private val underTest by lazy { kosmos.notificationsShadeSceneViewModel }
@Test
fun upTransitionSceneKey_deviceLocked_lockscreen() =
@@ -65,11 +67,23 @@
lockDevice()
assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Down)).isNull()
assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
.isEqualTo(Scenes.Lockscreen)
}
@Test
+ fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
fun upTransitionSceneKey_deviceUnlocked_gone() =
testScope.runTest {
val destinationScenes by collectLastValue(underTest.destinationScenes)
@@ -77,6 +91,33 @@
unlockDevice()
assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Down)).isNull()
+ assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() =
+ testScope.runTest {
+ kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Up)).isNull()
+ assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
+ .isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() =
+ testScope.runTest {
+ kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ lockDevice()
+ unlockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Up)).isNull()
assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt
new file mode 100644
index 0000000..14d6094
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.qs.panels.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PaginatedGridRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ val underTest = kosmos.paginatedGridRepository
+
+ @Test
+ fun rows_followsConfig() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows by collectLastValue(underTest.rows)
+
+ setRowsInConfig(3)
+ assertThat(rows).isEqualTo(3)
+
+ setRowsInConfig(6)
+ assertThat(rows).isEqualTo(6)
+ }
+ }
+
+ private fun setRowsInConfig(rows: Int) =
+ with(kosmos) {
+ testCase.context.orCreateTestableResources.addOverride(
+ R.integer.quick_settings_max_rows,
+ rows,
+ )
+ fakeConfigurationRepository.onConfigurationChange()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
new file mode 100644
index 0000000..914a095
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.iconTilesRepository
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InfiniteGridLayoutTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().apply {
+ iconTilesRepository =
+ object : IconTilesRepository {
+ override fun isIconTile(spec: TileSpec): Boolean {
+ return spec.spec.startsWith("small")
+ }
+ }
+ }
+
+ private val underTest =
+ with(kosmos) {
+ InfiniteGridLayout(
+ iconTilesViewModel,
+ fixedColumnsSizeViewModel,
+ )
+ }
+
+ @Test
+ fun correctPagination_underOnePage_sameOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile()
+ )
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ assertThat(pages).hasSize(1)
+ assertThat(pages[0]).isEqualTo(tiles)
+ }
+ }
+
+ @Test
+ fun correctPagination_twoPages_sameOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ )
+ // --- Page 1 ---
+ // [L L] [S] [S]
+ // [L L] [L L]
+ // [S] [S] [L L]
+ // --- Page 2 ---
+ // [L L] [S] [S]
+ // [L L]
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ assertThat(pages).hasSize(2)
+ assertThat(pages[0]).isEqualTo(tiles.take(8))
+ assertThat(pages[1]).isEqualTo(tiles.drop(8))
+ }
+ }
+
+ companion object {
+ fun largeTile() = MockTileViewModel(TileSpec.create("large"))
+
+ fun smallTile() = MockTileViewModel(TileSpec.create("small"))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt
new file mode 100644
index 0000000..6df3f8d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PaginatableGridLayoutTest : SysuiTestCase() {
+ @Test
+ fun correctRows_gapsAtEnd() {
+ val columns = 6
+
+ val sizedTiles =
+ listOf(
+ largeTile(),
+ extraLargeTile(),
+ largeTile(),
+ smallTile(),
+ largeTile(),
+ )
+
+ // [L L] [XL XL XL]
+ // [L L] [S] [L L]
+
+ val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
+
+ assertThat(rows).hasSize(2)
+ assertThat(rows[0]).isEqualTo(sizedTiles.take(2))
+ assertThat(rows[1]).isEqualTo(sizedTiles.drop(2))
+ }
+
+ @Test
+ fun correctRows_fullLastRow_noEmptyRow() {
+ val columns = 6
+
+ val sizedTiles =
+ listOf(
+ largeTile(),
+ extraLargeTile(),
+ smallTile(),
+ )
+
+ // [L L] [XL XL XL] [S]
+
+ val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
+
+ assertThat(rows).hasSize(1)
+ assertThat(rows[0]).isEqualTo(sizedTiles)
+ }
+
+ companion object {
+ fun extraLargeTile() = SizedTile(MockTileViewModel(TileSpec.create("XLarge")), 3)
+
+ fun largeTile() = SizedTile(MockTileViewModel(TileSpec.create("large")), 2)
+
+ fun smallTile() = SizedTile(MockTileViewModel(TileSpec.create("small")), 1)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt
new file mode 100644
index 0000000..3354b4d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.iconTilesRepository
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.partitionedGridViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PartitionedGridLayoutTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().apply {
+ iconTilesRepository =
+ object : IconTilesRepository {
+ override fun isIconTile(spec: TileSpec): Boolean {
+ return spec.spec.startsWith("small")
+ }
+ }
+ }
+
+ private val underTest = with(kosmos) { PartitionedGridLayout(partitionedGridViewModel) }
+
+ @Test
+ fun correctPagination_underOnePage_partitioned_sameRelativeOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile()
+ )
+ val (smallTiles, largeTiles) =
+ tiles.partition { iconTilesViewModel.isIconTile(it.spec) }
+
+ // [L L] [L L]
+ // [L L]
+ // [S] [S] [S]
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ Truth.assertThat(pages).hasSize(1)
+ Truth.assertThat(pages[0]).isEqualTo(largeTiles + smallTiles)
+ }
+ }
+
+ @Test
+ fun correctPagination_twoPages_partitioned_sameRelativeOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ )
+ // --- Page 1 ---
+ // [L L] [L L]
+ // [L L]
+ // [S] [S] [S] [S]
+ // --- Page 2 ---
+ // [S] [S]
+
+ val (smallTiles, largeTiles) =
+ tiles.partition { iconTilesViewModel.isIconTile(it.spec) }
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ val expectedPage0 = largeTiles + smallTiles.take(4)
+ val expectedPage1 = smallTiles.drop(4)
+
+ Truth.assertThat(pages).hasSize(2)
+ Truth.assertThat(pages[0]).isEqualTo(expectedPage0)
+ Truth.assertThat(pages[1]).isEqualTo(expectedPage1)
+ }
+ }
+
+ companion object {
+ fun largeTile() = MockTileViewModel(TileSpec.create("large"))
+
+ fun smallTile() = MockTileViewModel(TileSpec.create("small"))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt
new file mode 100644
index 0000000..2194c75
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.recordissue.IssueRecordingState
+import com.android.systemui.settings.fakeUserFileManager
+import com.android.systemui.settings.userTracker
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingDataInteractorTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos().also { it.testCase = this }
+ private val userTracker = kosmos.userTracker
+ private val userFileManager = kosmos.fakeUserFileManager
+ private val testUser = UserHandle.of(1)
+
+ lateinit var state: IssueRecordingState
+ private lateinit var underTest: IssueRecordingDataInteractor
+
+ @Before
+ fun setup() {
+ state = IssueRecordingState(userTracker, userFileManager)
+ underTest = IssueRecordingDataInteractor(state, kosmos.testScope.testScheduler)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun emitsEvent_whenIsRecordingStatusChanges_correctly() {
+ kosmos.testScope.runTest {
+ val data by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ runCurrent()
+ Truth.assertThat(data?.isRecording).isFalse()
+
+ state.isRecording = true
+ runCurrent()
+ Truth.assertThat(data?.isRecording).isTrue()
+
+ state.isRecording = false
+ runCurrent()
+ Truth.assertThat(data?.isRecording).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
new file mode 100644
index 0000000..2444229
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.qs.tiles.impl.irecording
+
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.recordissue.RecordIssueModule
+import com.android.systemui.res.R
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos().also { it.testCase = this }
+ private val uiConfig =
+ QSTileUIConfig.Resource(R.drawable.qs_record_issue_icon_off, R.string.qs_record_issue_label)
+ private val config =
+ QSTileConfig(
+ TileSpec.create(RecordIssueModule.TILE_SPEC),
+ uiConfig,
+ kosmos.qsEventLogger.getNewInstanceId()
+ )
+ private val resources = kosmos.mainResources
+ private val theme = resources.newTheme()
+
+ @Test
+ fun whenData_isRecording_useCorrectResources() {
+ val underTest = IssueRecordingMapper(resources, theme)
+ val tileState = underTest.map(config, IssueRecordingModel(true))
+ Truth.assertThat(tileState.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+ }
+
+ @Test
+ fun whenData_isNotRecording_useCorrectResources() {
+ val underTest = IssueRecordingMapper(resources, theme)
+ val tileState = underTest.map(config, IssueRecordingModel(false))
+ Truth.assertThat(tileState.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
new file mode 100644
index 0000000..4e58069
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.qs.pipeline.domain.interactor.panelInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingUserActionInteractorTest : SysuiTestCase() {
+
+ val user = UserHandle(1)
+ val kosmos = Kosmos().also { it.testCase = this }
+
+ private lateinit var userContextProvider: UserContextProvider
+ private lateinit var underTest: IssueRecordingUserActionInteractor
+
+ private var hasCreatedDialogDelegate: Boolean = false
+
+ @Before
+ fun setup() {
+ hasCreatedDialogDelegate = false
+ with(kosmos) {
+ val factory =
+ object : RecordIssueDialogDelegate.Factory {
+ override fun create(onStarted: Runnable): RecordIssueDialogDelegate {
+ hasCreatedDialogDelegate = true
+
+ // Inside some tests in presubmit, createDialog throws an error because
+ // the test thread's looper hasn't been prepared, and Dialog.class
+ // internally is creating a new handler. For testing, we only care that the
+ // dialog is created, so using a mock is acceptable here.
+ return mock(RecordIssueDialogDelegate::class.java)
+ }
+ }
+
+ userContextProvider = userTracker
+ underTest =
+ IssueRecordingUserActionInteractor(
+ testDispatcher,
+ KeyguardDismissUtil(
+ keyguardStateController,
+ statusBarStateController,
+ activityStarter
+ ),
+ keyguardStateController,
+ dialogTransitionAnimator,
+ panelInteractor,
+ userTracker,
+ factory
+ )
+ }
+ }
+
+ @Test
+ fun handleInput_showsPromptToStartRecording_whenNotRecordingAlready() {
+ kosmos.testScope.runTest {
+ underTest.handleInput(
+ QSTileInput(user, QSTileUserAction.Click(null), IssueRecordingModel(false))
+ )
+ Truth.assertThat(hasCreatedDialogDelegate).isTrue()
+ }
+ }
+
+ @Test
+ fun handleInput_attemptsToStopRecording_whenRecording() {
+ kosmos.testScope.runTest {
+ val input = QSTileInput(user, QSTileUserAction.Click(null), IssueRecordingModel(true))
+ try {
+ underTest.handleInput(input)
+ } catch (e: NullPointerException) {
+ // As of 06/07/2024, PendingIntent.startService is not easily mockable and throws
+ // an NPE inside IActivityManager. Catching that here and ignore it, then verify
+ // mock interactions were done correctly
+ }
+ Truth.assertThat(hasCreatedDialogDelegate).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 5b6fea5..e01ffa6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
@@ -42,9 +43,9 @@
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneBackInteractor
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
@@ -160,6 +161,37 @@
}
@Test
+ fun destinations_whenNotCustomizing_withPreviousSceneLockscreen_butLockscreenDisabled() =
+ testScope.runTest {
+ overrideResource(R.bool.config_use_split_notification_shade, false)
+ qsFlexiglassAdapter.setCustomizing(false)
+ val destinations by collectLastValue(underTest.destinationScenes)
+
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val backScene by collectLastValue(sceneBackInteractor.backScene)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+ sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(backScene).isNull()
+ assertThat(destinations)
+ .isEqualTo(
+ mapOf(
+ Back to UserActionResult(Scenes.Shade),
+ Swipe(SwipeDirection.Up) to UserActionResult(Scenes.Shade),
+ Swipe(
+ fromSource = Edge.Bottom,
+ direction = SwipeDirection.Up,
+ ) to UserActionResult(SceneFamilies.Home)
+ )
+ )
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
fun destinations_whenNotCustomizing_authMethodSwipe_lockscreenNotDismissed() =
testScope.runTest {
overrideResource(R.bool.config_use_split_notification_shade, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
index ac67ac8..411a7a4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
@@ -28,12 +28,14 @@
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.ui.viewmodel.quickSettingsShadeSceneViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -56,7 +58,7 @@
private val sceneInteractor = kosmos.sceneInteractor
private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
- private val underTest = kosmos.quickSettingsShadeSceneViewModel
+ private val underTest by lazy { kosmos.quickSettingsShadeSceneViewModel }
@Test
fun upTransitionSceneKey_deviceLocked_lockscreen() =
@@ -66,10 +68,23 @@
lockDevice()
assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Down)).isNull()
assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
}
@Test
+ fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ lockDevice()
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+ assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
fun upTransitionSceneKey_deviceUnlocked_gone() =
testScope.runTest {
val destinationScenes by collectLastValue(underTest.destinationScenes)
@@ -78,6 +93,34 @@
unlockDevice()
assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Down)).isNull()
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() =
+ testScope.runTest {
+ kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ lockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Up)).isNull()
+ assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() =
+ testScope.runTest {
+ kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ lockDevice()
+ unlockDevice()
+
+ assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(destinationScenes?.get(Swipe.Up)).isNull()
assertThat(homeScene).isEqualTo(Scenes.Gone)
}
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 4d5d22c..412505d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -58,9 +58,9 @@
import com.android.systemui.qs.footerActionsController
import com.android.systemui.qs.footerActionsViewModelFactory
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt
index e3108ad..1f3454d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneBackInteractorTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index ec7150b..5242fe3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -26,6 +26,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.repository.Idle
@@ -450,4 +451,16 @@
progress.value = 0.9f
assertThat(transitionValue).isEqualTo(0f)
}
+
+ @Test
+ fun changeScene_toGone_whenKeyguardDisabled_doesNotThrow() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+ underTest.changeScene(Scenes.Gone, "")
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableTest.kt
new file mode 100644
index 0000000..695edaf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableTest.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.internal.policy.IKeyguardStateCallback
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.setSceneTransition
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
+class KeyguardStateCallbackStartableTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val underTest = kosmos.keyguardStateCallbackStartable
+
+ @Test
+ fun addCallback_hydratesAllWithCurrentState() =
+ testScope.runTest {
+ val testState = setUpTest()
+ val callback = mockCallback()
+
+ underTest.addCallback(callback)
+ runCurrent()
+
+ with(testState) {
+ val captor = argumentCaptor<Boolean>()
+ verify(callback, atLeastOnce()).onShowingStateChanged(captor.capture(), eq(userId))
+ assertThat(captor.lastValue).isEqualTo(isKeyguardShowing)
+ verify(callback, atLeastOnce()).onInputRestrictedStateChanged(captor.capture())
+ assertThat(captor.lastValue).isEqualTo(isInputRestricted)
+ verify(callback, atLeastOnce()).onSimSecureStateChanged(captor.capture())
+ assertThat(captor.lastValue).isEqualTo(isSimSecure)
+ verify(callback, atLeastOnce()).onTrustedChanged(captor.capture())
+ assertThat(captor.lastValue).isEqualTo(isTrusted)
+ }
+ }
+
+ @Test
+ fun hydrateKeyguardShowingState() =
+ testScope.runTest {
+ setUpTest(isKeyguardShowing = true)
+ val callback = mockCallback()
+ underTest.addCallback(callback)
+ runCurrent()
+ verify(callback, atLeastOnce()).onShowingStateChanged(eq(true), anyInt())
+
+ unlockDevice()
+ runCurrent()
+
+ verify(callback).onShowingStateChanged(eq(false), anyInt())
+ }
+
+ @Test
+ fun hydrateInputRestrictedState() =
+ testScope.runTest {
+ setUpTest(isKeyguardShowing = true)
+ val callback = mockCallback()
+ underTest.addCallback(callback)
+ runCurrent()
+ val captor = argumentCaptor<Boolean>()
+ verify(callback, atLeastOnce()).onInputRestrictedStateChanged(captor.capture())
+ assertThat(captor.lastValue).isTrue()
+
+ unlockDevice()
+ runCurrent()
+
+ verify(callback, atLeastOnce()).onInputRestrictedStateChanged(captor.capture())
+ assertThat(captor.lastValue).isFalse()
+ }
+
+ @Test
+ fun hydrateSimSecureState() =
+ testScope.runTest {
+ setUpTest(isSimSecure = false)
+ val callback = mockCallback()
+ underTest.addCallback(callback)
+ runCurrent()
+ val captor = argumentCaptor<Boolean>()
+ verify(callback, atLeastOnce()).onSimSecureStateChanged(captor.capture())
+ assertThat(captor.lastValue).isFalse()
+
+ kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true
+ runCurrent()
+
+ verify(callback, atLeastOnce()).onSimSecureStateChanged(captor.capture())
+ assertThat(captor.lastValue).isTrue()
+ }
+
+ @Test
+ fun notifyWhenKeyguardShowingChanged() =
+ testScope.runTest {
+ setUpTest(isKeyguardShowing = true)
+ val callback = mockCallback()
+ underTest.addCallback(callback)
+ runCurrent()
+ assertThat(kosmos.fakeTrustRepository.keyguardShowingChangeEventCount).isEqualTo(1)
+
+ unlockDevice()
+ runCurrent()
+
+ assertThat(kosmos.fakeTrustRepository.keyguardShowingChangeEventCount).isEqualTo(2)
+ }
+
+ @Test
+ fun notifyWhenTrustChanged() =
+ testScope.runTest {
+ setUpTest(isTrusted = false)
+ val callback = mockCallback()
+ underTest.addCallback(callback)
+ runCurrent()
+ val captor = argumentCaptor<Boolean>()
+ verify(callback, atLeastOnce()).onTrustedChanged(captor.capture())
+ assertThat(captor.lastValue).isFalse()
+
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
+ runCurrent()
+
+ verify(callback, atLeastOnce()).onTrustedChanged(captor.capture())
+ assertThat(captor.lastValue).isTrue()
+ }
+
+ private suspend fun TestScope.setUpTest(
+ isKeyguardShowing: Boolean = true,
+ userId: Int = selectedUser.id,
+ isInputRestricted: Boolean = true,
+ isSimSecure: Boolean = false,
+ isTrusted: Boolean = false,
+ ): TestState {
+ val testState =
+ TestState(
+ isKeyguardShowing = isKeyguardShowing,
+ userId = userId,
+ isInputRestricted = isInputRestricted,
+ isSimSecure = isSimSecure,
+ isTrusted = isTrusted,
+ )
+
+ if (isKeyguardShowing) {
+ lockDevice()
+ } else {
+ unlockDevice()
+ }
+
+ kosmos.fakeUserRepository.setUserInfos(listOf(selectedUser))
+ kosmos.fakeUserRepository.setSelectedUserInfo(selectedUser)
+
+ if (isInputRestricted && !isKeyguardShowing) {
+ // TODO(b/348644111): add support for mNeedToReshowWhenReenabled
+ } else if (!isInputRestricted) {
+ assertWithMessage(
+ "If isInputRestricted is false, isKeyguardShowing must also be false!"
+ )
+ .that(isKeyguardShowing)
+ .isFalse()
+ }
+
+ kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = isSimSecure
+
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(isTrusted)
+
+ runCurrent()
+
+ underTest.start()
+
+ return testState
+ }
+
+ private fun lockDevice() {
+ kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Lockscreen))
+ kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "")
+ }
+
+ private fun unlockDevice() {
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ kosmos.setSceneTransition(ObservableTransitionState.Idle(Scenes.Gone))
+ kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+ }
+
+ private fun mockCallback(): IKeyguardStateCallback {
+ return mock()
+ }
+
+ private data class TestState(
+ val isKeyguardShowing: Boolean,
+ val userId: Int,
+ val isInputRestricted: Boolean,
+ val isSimSecure: Boolean,
+ val isTrusted: Boolean,
+ )
+
+ companion object {
+ private val selectedUser =
+ UserInfo(
+ /* id= */ 100,
+ /* name= */ "First user",
+ /* flags= */ 0,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index e40c8ee..ae615dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -36,12 +36,15 @@
import com.android.systemui.classifier.falsingManager
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.sysUiState
@@ -51,7 +54,6 @@
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
@@ -1340,6 +1342,111 @@
assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
}
+ @Test
+ fun switchToGone_whenKeyguardBecomesDisabled() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ prepareState()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ underTest.start()
+
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun switchToGone_whenKeyguardBecomesDisabled_whenOnShadeScene() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ prepareState(
+ initialSceneKey = Scenes.Shade,
+ )
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ underTest.start()
+
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun doesNotSwitchToGone_whenKeyguardBecomesDisabled_whenInLockdownMode() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ prepareState()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ underTest.start()
+
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsUserInLockdown(true)
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun doesNotSwitchToGone_whenKeyguardBecomesDisabled_whenDeviceEntered() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ prepareState(
+ isDeviceUnlocked = true,
+ initialSceneKey = Scenes.Gone,
+ )
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isTrue()
+ underTest.start()
+ sceneInteractor.changeScene(Scenes.Shade, "")
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isTrue()
+
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ }
+
+ @Test
+ fun switchToLockscreen_whenKeyguardBecomesEnabled_afterHidingWhenDisabled() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ prepareState()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ underTest.start()
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(true)
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun doesNotSwitchToLockscreen_whenKeyguardBecomesEnabled_ifAuthMethodBecameInsecure() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ prepareState()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ underTest.start()
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ runCurrent()
+
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(true)
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ }
+
private fun TestScope.emulateSceneTransition(
transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
toScene: SceneKey,
@@ -1382,15 +1489,10 @@
isDeviceProvisioned: Boolean = true,
isInteractive: Boolean = true,
): MutableStateFlow<ObservableTransitionState> {
- if (authenticationMethod?.isSecure == true) {
- assert(isLockscreenEnabled) {
- "Lockscreen cannot be disabled while having a secure authentication method"
- }
- if (isDeviceUnlocked) {
- kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
- SuccessFingerprintAuthenticationStatus(0, true)
- )
- }
+ if (isDeviceUnlocked) {
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
}
check(initialSceneKey != Scenes.Gone || isDeviceUnlocked) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index c53cdf8..a0295c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
@@ -139,6 +140,21 @@
}
@Test
+ fun upTransitionSceneKey_keyguardDisabled_gone() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
+ .isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
testScope.runTest {
val destinationScenes by collectLastValue(underTest.destinationScenes)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
new file mode 100644
index 0000000..5e87f46
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.app.PendingIntent
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository
+import com.android.systemui.statusbar.notification.row.shared.IconModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel.TimerState.Paused
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME)
+class TimerViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val repository = kosmos.fakeNotificationRowRepository
+
+ private var contentModel: TimerContentModel?
+ get() = repository.richOngoingContentModel.value as? TimerContentModel
+ set(value) {
+ repository.richOngoingContentModel.value = value
+ }
+
+ private lateinit var underTest: TimerViewModel
+
+ @Before
+ fun setup() {
+ underTest = kosmos.getTimerViewModel(repository)
+ }
+
+ @Test
+ fun labelShowsTheTimerName() =
+ testScope.runTest {
+ val label by collectLastValue(underTest.label)
+ contentModel = pausedTimer(name = "Example Timer Name")
+ assertThat(label).isEqualTo("Example Timer Name")
+ }
+
+ @Test
+ fun pausedTimeRemainingFormatsWell() =
+ testScope.runTest {
+ val label by collectLastValue(underTest.pausedTime)
+ contentModel = pausedTimer(timeRemaining = Duration.ofMinutes(3))
+ assertThat(label).isEqualTo("3:00")
+ contentModel = pausedTimer(timeRemaining = Duration.ofSeconds(119))
+ assertThat(label).isEqualTo("1:59")
+ contentModel = pausedTimer(timeRemaining = Duration.ofSeconds(121))
+ assertThat(label).isEqualTo("2:01")
+ contentModel = pausedTimer(timeRemaining = Duration.ofHours(1))
+ assertThat(label).isEqualTo("1:00:00")
+ contentModel = pausedTimer(timeRemaining = Duration.ofHours(24))
+ assertThat(label).isEqualTo("24:00:00")
+ }
+
+ private fun pausedTimer(
+ icon: IconModel = mock(),
+ name: String = "example",
+ timeRemaining: Duration = Duration.ofMinutes(3),
+ resumeIntent: PendingIntent? = null,
+ resetIntent: PendingIntent? = null
+ ) =
+ TimerContentModel(
+ icon = icon,
+ name = name,
+ state =
+ Paused(
+ timeRemaining = timeRemaining,
+ resumeIntent = resumeIntent,
+ resetIntent = resetIntent,
+ )
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 68e4cda..40315a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -148,36 +148,40 @@
assertThat(important).isTrue()
}
+ // NOTE: The empty shade view and the footer view should be mutually exclusive.
+
@Test
- fun shouldIncludeEmptyShadeView_trueWhenNoNotifs() =
+ fun shouldShowEmptyShadeView_trueWhenNoNotifs() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
runCurrent()
// THEN empty shade is visible
- assertThat(shouldInclude).isTrue()
+ assertThat(shouldShowEmptyShadeView).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenNotifs() =
+ fun shouldShowEmptyShadeView_falseWhenNotifs() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
+ fun shouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -186,13 +190,14 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShow).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+ fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -205,13 +210,15 @@
runCurrent()
// THEN empty shade is visible
- assertThat(shouldInclude).isTrue()
+ assertThat(shouldShowEmptyShadeView).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_trueWhenLockedShade() =
+ fun shouldShowEmptyShadeView_trueWhenLockedShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -220,13 +227,14 @@
runCurrent()
// THEN empty shade is visible
- assertThat(shouldInclude).isTrue()
+ assertThat(shouldShowEmptyShadeView).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenKeyguard() =
+ fun shouldShowEmptyShadeView_falseWhenKeyguard() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -235,13 +243,13 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShow).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
+ fun shouldShowEmptyShadeView_falseWhenStartingToSleep() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -252,7 +260,7 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShow).isFalse()
}
@Test
@@ -308,7 +316,8 @@
@Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -318,13 +327,15 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldInclude?.value).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isTrue()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
fun shouldIncludeFooterView_trueWhenLockedShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -334,7 +345,8 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldInclude?.value).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isTrue()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
@@ -410,7 +422,8 @@
@Test
fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -425,7 +438,8 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldInclude?.value).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isTrue()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 1656a2e..5887f90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -20,8 +20,11 @@
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
+import android.os.Handler
import android.os.RemoteException
import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.View
import android.widget.FrameLayout
import android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR
@@ -29,6 +32,7 @@
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.LaunchableView
@@ -36,7 +40,6 @@
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.WakefulnessLifecycle
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeController
import com.android.systemui.shade.data.repository.FakeShadeRepository
@@ -51,11 +54,6 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
@@ -64,14 +62,19 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.nullable
import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
@ExperimentalCoroutinesApi
@SmallTest
@@ -132,22 +135,22 @@
mainExecutor = mainExecutor,
communalSceneInteractor = communalSceneInteractor,
)
- whenever(userTracker.userHandle).thenReturn(UserHandle.OWNER)
- whenever(communalSceneInteractor.isIdleOnCommunal).thenReturn(MutableStateFlow(false))
+ `when`(userTracker.userHandle).thenReturn(UserHandle.OWNER)
+ `when`(communalSceneInteractor.isIdleOnCommunal).thenReturn(MutableStateFlow(false))
}
@Test
fun startPendingIntentDismissingKeyguard_keyguardShowing_dismissWithAction() {
val pendingIntent = mock(PendingIntent::class.java)
- whenever(pendingIntent.isActivity).thenReturn(true)
- whenever(keyguardStateController.isShowing).thenReturn(true)
- whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ `when`(pendingIntent.isActivity).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
underTest.startPendingIntentDismissingKeyguard(intent = pendingIntent, dismissShade = true)
mainExecutor.runAllReady()
verify(statusBarKeyguardViewManager)
- .dismissWithAction(any(OnDismissAction::class.java), eq(null), anyBoolean(), eq(null))
+ .dismissWithAction(any(), eq(null), anyBoolean(), eq(null))
}
@Test
@@ -160,10 +163,10 @@
}
parent.addView(view)
val controller = ActivityTransitionAnimator.Controller.fromView(view)
- whenever(pendingIntent.isActivity).thenReturn(true)
- whenever(keyguardStateController.isShowing).thenReturn(true)
- whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
- whenever(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
+ `when`(pendingIntent.isActivity).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ `when`(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
.thenReturn(true)
startPendingIntentMaybeDismissingKeyguard(
@@ -175,9 +178,9 @@
verify(activityTransitionAnimator)
.startPendingIntentWithAnimation(
- nullable(),
+ nullable(ActivityTransitionAnimator.Controller::class.java),
eq(true),
- nullable(),
+ nullable(String::class.java),
eq(true),
any(),
)
@@ -193,10 +196,10 @@
}
parent.addView(view)
val controller = ActivityTransitionAnimator.Controller.fromView(view)
- whenever(pendingIntent.isActivity).thenReturn(true)
- whenever(keyguardStateController.isShowing).thenReturn(true)
- whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
- whenever(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
+ `when`(pendingIntent.isActivity).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
+ `when`(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt()))
.thenReturn(false)
// extra activity options to set on pending intent
@@ -220,12 +223,12 @@
eq(context),
eq(0),
eq(fillInIntent),
- nullable(),
- nullable(),
- nullable(),
+ nullable(PendingIntent.OnFinished::class.java),
+ nullable(Handler::class.java),
+ nullable(String::class.java),
bundleCaptor.capture()
)
- val options = ActivityOptions.fromBundle(bundleCaptor.value)
+ val options = ActivityOptions.fromBundle(bundleCaptor.firstValue)
assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
}
@@ -245,6 +248,74 @@
verify(centralSurfaces).getAnimatorControllerFromNotification(associatedView)
}
+ @EnableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
+ @Test
+ fun startPendingIntentDismissingKeyguard_transitionAnimator_animateOverOcclusion() {
+ val parent = FrameLayout(context)
+ val view =
+ object : View(context), LaunchableView {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+ }
+ parent.addView(view)
+ val controller = ActivityTransitionAnimator.Controller.fromView(view)
+ val pendingIntent = mock(PendingIntent::class.java)
+ `when`(pendingIntent.isActivity).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(keyguardStateController.isOccluded).thenReturn(true)
+
+ underTest.startPendingIntentDismissingKeyguard(
+ intent = pendingIntent,
+ dismissShade = true,
+ animationController = controller,
+ showOverLockscreen = true,
+ skipLockscreenChecks = true
+ )
+ mainExecutor.runAllReady()
+
+ verify(activityTransitionAnimator)
+ .startPendingIntentWithAnimation(
+ nullable(ActivityTransitionAnimator.Controller::class.java),
+ eq(true),
+ nullable(String::class.java),
+ eq(true),
+ any(),
+ )
+ }
+
+ @DisableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
+ @Test
+ fun startPendingIntentDismissingKeyguard_transitionAnimator_doNotAnimateOverOcclusion() {
+ val parent = FrameLayout(context)
+ val view =
+ object : View(context), LaunchableView {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+ }
+ parent.addView(view)
+ val controller = ActivityTransitionAnimator.Controller.fromView(view)
+ val pendingIntent = mock(PendingIntent::class.java)
+ `when`(pendingIntent.isActivity).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(keyguardStateController.isOccluded).thenReturn(true)
+
+ underTest.startPendingIntentDismissingKeyguard(
+ intent = pendingIntent,
+ dismissShade = true,
+ animationController = controller,
+ showOverLockscreen = true,
+ skipLockscreenChecks = true
+ )
+ mainExecutor.runAllReady()
+
+ verify(activityTransitionAnimator)
+ .startPendingIntentWithAnimation(
+ nullable(ActivityTransitionAnimator.Controller::class.java),
+ eq(false),
+ nullable(String::class.java),
+ eq(true),
+ any(),
+ )
+ }
+
@Test
fun startActivity_noUserHandleProvided_getUserHandle() {
val intent = mock(Intent::class.java)
@@ -254,13 +325,66 @@
verify(userTracker).userHandle
}
+ @EnableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
+ @Test
+ fun startActivity_transitionAnimator_animateOverOcclusion() {
+ val intent = mock(Intent::class.java)
+ val parent = FrameLayout(context)
+ val view =
+ object : View(context), LaunchableView {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+ }
+ parent.addView(view)
+ val controller = ActivityTransitionAnimator.Controller.fromView(view)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(keyguardStateController.isOccluded).thenReturn(true)
+
+ mainExecutor.runAllReady()
+ underTest.startActivity(intent, true, controller, true, null)
+
+ verify(activityTransitionAnimator)
+ .startIntentWithAnimation(
+ nullable(ActivityTransitionAnimator.Controller::class.java),
+ eq(true),
+ nullable(String::class.java),
+ eq(true),
+ any(),
+ )
+ }
+
+ @DisableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
+ @Test
+ fun startActivity_transitionAnimator_doNotAnimateOverOcclusion() {
+ val intent = mock(Intent::class.java)
+ val parent = FrameLayout(context)
+ val view =
+ object : View(context), LaunchableView {
+ override fun setShouldBlockVisibilityChanges(block: Boolean) {}
+ }
+ parent.addView(view)
+ val controller = ActivityTransitionAnimator.Controller.fromView(view)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
+ `when`(keyguardStateController.isOccluded).thenReturn(true)
+
+ mainExecutor.runAllReady()
+ underTest.startActivity(intent, true, controller, true, null)
+
+ verify(activityTransitionAnimator)
+ .startIntentWithAnimation(
+ nullable(ActivityTransitionAnimator.Controller::class.java),
+ eq(false),
+ nullable(String::class.java),
+ eq(true),
+ any(),
+ )
+ }
+
@Test
fun dismissKeyguardThenExecute_startWakeAndUnlock() {
- whenever(wakefulnessLifecycle.wakefulness)
- .thenReturn(WakefulnessLifecycle.WAKEFULNESS_ASLEEP)
- whenever(keyguardStateController.canDismissLockScreen()).thenReturn(true)
- whenever(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false)
- whenever(dozeServiceHost.isPulsing).thenReturn(true)
+ `when`(wakefulnessLifecycle.wakefulness).thenReturn(WakefulnessLifecycle.WAKEFULNESS_ASLEEP)
+ `when`(keyguardStateController.canDismissLockScreen()).thenReturn(true)
+ `when`(statusBarStateController.leaveOpenOnKeyguardHide()).thenReturn(false)
+ `when`(dozeServiceHost.isPulsing).thenReturn(true)
underTest.dismissKeyguardThenExecute({ true }, {}, false)
@@ -271,25 +395,20 @@
@Test
fun dismissKeyguardThenExecute_keyguardIsShowing_dismissWithAction() {
val customMessage = "Enter your pin."
- whenever(keyguardStateController.isShowing).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(true)
underTest.dismissKeyguardThenExecute({ true }, {}, false, customMessage)
verify(statusBarKeyguardViewManager)
- .dismissWithAction(
- any(OnDismissAction::class.java),
- any(Runnable::class.java),
- eq(false),
- eq(customMessage)
- )
+ .dismissWithAction(any(), any(), eq(false), eq(customMessage))
}
@Test
fun dismissKeyguardThenExecute_awakeDreams() {
val customMessage = "Enter your pin."
var dismissActionExecuted = false
- whenever(keyguardStateController.isShowing).thenReturn(false)
- whenever(keyguardUpdateMonitor.isDreaming).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(false)
+ `when`(keyguardUpdateMonitor.isDreaming).thenReturn(true)
underTest.dismissKeyguardThenExecute(
{
@@ -308,9 +427,9 @@
@Test
@Throws(RemoteException::class)
fun executeRunnableDismissingKeyguard_dreaming_notShowing_awakenDreams() {
- whenever(keyguardStateController.isShowing).thenReturn(false)
- whenever(keyguardStateController.isOccluded).thenReturn(false)
- whenever(keyguardUpdateMonitor.isDreaming).thenReturn(true)
+ `when`(keyguardStateController.isShowing).thenReturn(false)
+ `when`(keyguardStateController.isOccluded).thenReturn(false)
+ `when`(keyguardUpdateMonitor.isDreaming).thenReturn(true)
underTest.executeRunnableDismissingKeyguard(
runnable = {},
@@ -326,9 +445,9 @@
@Test
@Throws(RemoteException::class)
fun executeRunnableDismissingKeyguard_notDreaming_notShowing_doNotAwakenDreams() {
- whenever(keyguardStateController.isShowing).thenReturn(false)
- whenever(keyguardStateController.isOccluded).thenReturn(false)
- whenever(keyguardUpdateMonitor.isDreaming).thenReturn(false)
+ `when`(keyguardStateController.isShowing).thenReturn(false)
+ `when`(keyguardStateController.isOccluded).thenReturn(false)
+ `when`(keyguardUpdateMonitor.isDreaming).thenReturn(false)
underTest.executeRunnableDismissingKeyguard(
runnable = {},
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 2f69942..ebc78d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.volume.panel.component.spatial.domain
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
import android.media.AudioDeviceAttributes
import android.media.AudioDeviceInfo
import android.media.session.MediaSession
@@ -24,12 +26,14 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.spatializerRepository
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.volume.localMediaController
@@ -56,8 +60,15 @@
@Before
fun setup() {
with(kosmos) {
+ val leAudioProfile =
+ mock<LeAudioProfile> {
+ whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+ whenever(isEnabled(any())).thenReturn(true)
+ }
val cachedBluetoothDevice: CachedBluetoothDevice = mock {
whenever(address).thenReturn("test_address")
+ whenever(profiles).thenReturn(listOf(leAudioProfile))
+ whenever(device).thenReturn(mock<BluetoothDevice> {})
}
localMediaRepository.updateCurrentConnectedDevice(
mock<BluetoothMediaDevice> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 555d77c..d5566ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -65,16 +65,20 @@
private val kosmos = testKosmos()
private lateinit var underTest: SpatialAudioComponentInteractor
+
+ private val bluetoothDevice: BluetoothDevice = mock {}
private val a2dpProfile: A2dpProfile = mock {
whenever(profileId).thenReturn(BluetoothProfile.A2DP)
+ whenever(isEnabled(bluetoothDevice)).thenReturn(false)
}
private val leAudioProfile: LeAudioProfile = mock {
whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+ whenever(isEnabled(bluetoothDevice)).thenReturn(true)
}
private val hearingAidProfile: HearingAidProfile = mock {
whenever(profileId).thenReturn(BluetoothProfile.HEARING_AID)
+ whenever(isEnabled(bluetoothDevice)).thenReturn(false)
}
- private val bluetoothDevice: BluetoothDevice = mock {}
@Before
fun setup() {
diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip.xml b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
index cd5c37d..beb16b3 100644
--- a/packages/SystemUI/res/layout/ongoing_activity_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
@@ -46,6 +46,8 @@
android:tint="?android:attr/colorPrimary"
/>
+ <!-- Only one of [ongoing_activity_chip_time, ongoing_activity_chip_text] will ever
+ be shown at one time. -->
<com.android.systemui.statusbar.chips.ui.view.ChipChronometer
android:id="@+id/ongoing_activity_chip_time"
android:layout_width="wrap_content"
@@ -58,5 +60,19 @@
android:textColor="?android:attr/colorPrimary"
/>
+ <!-- Used to show generic text in the chip instead of a timer. -->
+ <TextView
+ android:id="@+id/ongoing_activity_chip_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:gravity="center|start"
+ android:paddingStart="@dimen/ongoing_activity_chip_icon_text_padding"
+ android:textAppearance="@android:style/TextAppearance.Material.Small"
+ android:fontFamily="@*android:string/config_headlineFontFamily"
+ android:textColor="?android:attr/colorPrimary"
+ android:visibility="gone"
+ />
+
</com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer>
</FrameLayout>
diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
new file mode 100644
index 0000000..f2bfbe5c9
--- /dev/null
+++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
@@ -0,0 +1,116 @@
+<?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
+ -->
+<com.android.systemui.statusbar.notification.row.ui.view.TimerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/topBaseline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="22sp"
+ />
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:src="@drawable/ic_close"
+ app:tint="@android:color/white"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/label"
+ android:baseline="18dp"
+ app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+ />
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toEndOf="@id/icon"
+ app:layout_constraintEnd_toStartOf="@id/chronoRemaining"
+ android:singleLine="true"
+ tools:text="15s Timer"
+ app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+ android:paddingEnd="4dp"
+ />
+ <Chronometer
+ android:id="@+id/chronoRemaining"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="20sp"
+ android:gravity="end"
+ tools:text="0:12"
+ app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+ app:layout_constraintEnd_toStartOf="@id/pausedTimeRemaining"
+ app:layout_constraintStart_toEndOf="@id/label"
+ android:countDown="true"
+ android:paddingEnd="4dp"
+ />
+ <TextView
+ android:id="@+id/pausedTimeRemaining"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textSize="20sp"
+ android:gravity="end"
+ tools:text="0:12"
+ app:layout_constraintBaseline_toTopOf="@id/topBaseline"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/chronoRemaining"
+ android:paddingEnd="4dp"
+ />
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/bottomOfTop"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="icon,label,chronoRemaining,pausedTimeRemaining"
+ />
+
+ <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+ android:id="@+id/mainButton"
+ android:layout_width="124dp"
+ android:layout_height="wrap_content"
+ tools:text="Reset"
+ tools:drawableStart="@android:drawable/ic_menu_add"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/altButton"
+ app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+ app:layout_constraintHorizontal_chainStyle="spread"
+ android:paddingEnd="4dp"
+ />
+
+ <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+ android:id="@+id/altButton"
+ tools:text="Reset"
+ tools:drawableStart="@android:drawable/ic_menu_add"
+ android:drawablePadding="2dp"
+ android:drawableTint="@android:color/white"
+ android:layout_width="124dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+ app:layout_constraintStart_toEndOf="@id/mainButton"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:paddingEnd="4dp"
+ />
+</com.android.systemui.statusbar.notification.row.ui.view.TimerView>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index f06e333..78d4fc8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -246,6 +246,9 @@
public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
+ @ViewDebug.ExportedProperty(category="recents")
+ public boolean isVisible;
+
public Task() {
// Do nothing
}
@@ -279,6 +282,7 @@
lastSnapshotData.set(other.lastSnapshotData);
positionInParent = other.positionInParent;
appBounds = other.appBounds;
+ isVisible = other.isVisible;
}
/**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
index 660f0db..2eac393 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java
@@ -53,6 +53,9 @@
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
@@ -142,12 +145,14 @@
};
private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() {
+ @WorkerThread
@Override
public void onRotationChanged(final int rotation) {
+ @Nullable Boolean rotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
mMainThreadHandler.postAtFrontOfQueue(() -> {
- onRotationWatcherChanged(rotation);
+ onRotationWatcherChanged(rotation, rotationLocked);
});
}
};
@@ -281,8 +286,8 @@
TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
}
- public void setRotationLockedAtAngle(int rotationSuggestion, String caller) {
- final Boolean isLocked = isRotationLocked();
+ public void setRotationLockedAtAngle(
+ @Nullable Boolean isLocked, int rotationSuggestion, String caller) {
if (isLocked == null) {
// Ignore if we can't read the setting for the current user
return;
@@ -291,21 +296,6 @@
/* rotation= */ rotationSuggestion, caller);
}
- /**
- * @return whether rotation is currently locked, or <code>null</code> if the setting couldn't
- * be read
- */
- public Boolean isRotationLocked() {
- try {
- return RotationPolicy.isRotationLocked(mContext);
- } catch (SecurityException e) {
- // TODO(b/279561841): RotationPolicy uses the current user to resolve the setting which
- // may change before the rotation watcher can be unregistered
- Log.e(TAG, "Failed to get isRotationLocked", e);
- return null;
- }
- }
-
public void setRotateSuggestionButtonState(boolean visible) {
setRotateSuggestionButtonState(visible, false /* force */);
}
@@ -469,7 +459,7 @@
* Called when the rotation watcher rotation changes, either from the watcher registered
* internally in this class, or a signal propagated from NavBarHelper.
*/
- public void onRotationWatcherChanged(int rotation) {
+ public void onRotationWatcherChanged(int rotation, @Nullable Boolean isRotationLocked) {
if (!mListenersRegistered) {
// Ignore if not registered
return;
@@ -477,17 +467,16 @@
// If the screen rotation changes while locked, potentially update lock to flow with
// new screen rotation and hide any showing suggestions.
- Boolean rotationLocked = isRotationLocked();
- if (rotationLocked == null) {
+ if (isRotationLocked == null) {
// Ignore if we can't read the setting for the current user
return;
}
// The isVisible check makes the rotation button disappear when we are not locked
// (e.g. for tabletop auto-rotate).
- if (rotationLocked || mRotationButton.isVisible()) {
+ if (isRotationLocked || mRotationButton.isVisible()) {
// Do not allow a change in rotation to set user rotation when docked.
- if (shouldOverrideUserLockPrefs(rotation) && rotationLocked && !mDocked) {
- setRotationLockedAtAngle(rotation, /* caller= */
+ if (shouldOverrideUserLockPrefs(rotation) && isRotationLocked && !mDocked) {
+ setRotationLockedAtAngle(true, rotation, /* caller= */
"RotationButtonController#onRotationWatcherChanged");
}
setRotateSuggestionButtonState(false /* visible */, true /* forced */);
@@ -592,7 +581,8 @@
private void onRotateSuggestionClick(View v) {
mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
incrementNumAcceptedRotationSuggestionsIfNeeded();
- setRotationLockedAtAngle(mLastRotationSuggestion,
+ setRotationLockedAtAngle(
+ RotationPolicyUtil.isRotationLocked(mContext), mLastRotationSuggestion,
/* caller= */ "RotationButtonController#onRotateSuggestionClick");
Log.i(TAG, "onRotateSuggestionClick() mLastRotationSuggestion=" + mLastRotationSuggestion);
v.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationPolicyUtil.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationPolicyUtil.kt
new file mode 100644
index 0000000..eac4a10
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationPolicyUtil.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.rotation
+
+import android.content.Context
+import android.util.Log
+import com.android.internal.view.RotationPolicy
+
+class RotationPolicyUtil {
+ companion object {
+ /**
+ * Recommend to be called on bg thread, or reuse the results. It's because
+ * [RotationPolicy.isRotationLocked] may make a binder call to query settings.
+ *
+ * @return whether rotation is currently locked, or <code>null</code> if the setting
+ * couldn't be read
+ */
+ @JvmStatic
+ fun isRotationLocked(context: Context): Boolean? {
+ try {
+ return RotationPolicy.isRotationLocked(context)
+ } catch (e: SecurityException) {
+ // TODO(b/279561841): RotationPolicy uses the current user to resolve the setting
+ // which may change before the rotation watcher can be unregistered
+ Log.e("RotationPolicy", "Failed to get isRotationLocked", e)
+ return null
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 70465bc..4217820 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1097,7 +1097,8 @@
int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation);
AnimatorSet anims = new AnimatorSet();
- ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation);
+ ObjectAnimator yAnim = ObjectAnimator.ofFloat(mViewFlipper, View.TRANSLATION_Y,
+ yTranslation);
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
0f);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 25ad385..b37ba89 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -1633,7 +1633,7 @@
int maxHeightSize;
int maxWidthSize;
- if (Flags.redesignMagnifierWindowSize()) {
+ if (Flags.redesignMagnificationWindowSize()) {
// mOuterBorderSize = transparent margin area
// mMirrorSurfaceMargin = transparent margin area + orange border width
// We would like to allow the width and height to be full size. Therefore, the max
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index c08756f..5e2b5ff 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -74,7 +74,7 @@
val isConfirmationRequired: Flow<Boolean>
/** Fingerprint sensor type */
- val sensorType: Flow<FingerprintSensorType>
+ val fingerprintSensorType: Flow<FingerprintSensorType>
/** Switch to the credential view. */
fun onSwitchToCredential()
@@ -154,7 +154,8 @@
}
}
- override val sensorType: Flow<FingerprintSensorType> = fingerprintPropertyRepository.sensorType
+ override val fingerprintSensorType: Flow<FingerprintSensorType> =
+ fingerprintPropertyRepository.sensorType
override fun onSwitchToCredential() {
val modalities: BiometricModalities =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 7081661..6c6ef5a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -21,7 +21,6 @@
import android.annotation.RawRes
import android.content.res.Configuration
import android.graphics.Rect
-import android.hardware.face.Face
import android.util.RotationUtils
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
@@ -137,7 +136,7 @@
displayStateInteractor.currentRotation,
displayStateInteractor.isFolded,
displayStateInteractor.isInRearDisplayMode,
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.showingError
@@ -183,7 +182,7 @@
displayStateInteractor.currentRotation,
displayStateInteractor.isFolded,
displayStateInteractor.isInRearDisplayMode,
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.isPendingConfirmation,
@@ -330,7 +329,7 @@
AuthType.Coex ->
combine(
displayStateInteractor.currentRotation,
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.showingError
@@ -430,7 +429,7 @@
AuthType.Fingerprint,
AuthType.Coex ->
combine(
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.isPendingConfirmation,
@@ -508,7 +507,7 @@
when (activeAuthType) {
AuthType.Fingerprint ->
combine(
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.showingError
@@ -546,7 +545,7 @@
}
AuthType.Coex ->
combine(
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.isPendingConfirmation,
@@ -606,7 +605,7 @@
AuthType.Fingerprint,
AuthType.Coex ->
combine(
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
promptViewModel.isAuthenticated,
promptViewModel.isAuthenticating,
promptViewModel.showingError
@@ -642,7 +641,7 @@
AuthType.Fingerprint,
AuthType.Coex ->
combine(
- promptSelectorInteractor.sensorType,
+ promptSelectorInteractor.fingerprintSensorType,
displayStateInteractor.currentRotation
) { sensorType: FingerprintSensorType, rotation: DisplayRotation ->
when (sensorType) {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
index 7d3075a..ed931bd 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
@@ -43,10 +43,11 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -68,7 +69,12 @@
mobileConnectionsRepository: MobileConnectionsRepository,
) {
val subId: StateFlow<Int> = repository.subscriptionId
- val isAnySimSecure: Flow<Boolean> = mobileConnectionsRepository.isAnySimSecure
+ val isAnySimSecure: StateFlow<Boolean> =
+ mobileConnectionsRepository.isAnySimSecure.stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = mobileConnectionsRepository.getIsAnySimSecure(),
+ )
val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim
val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
index 108e22b..64dedea 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
@@ -16,6 +16,7 @@
package com.android.systemui.common.ui.binder
+import android.view.View
import android.widget.ImageView
import com.android.systemui.common.shared.model.Icon
@@ -30,4 +31,13 @@
is Icon.Resource -> view.setImageResource(icon.res)
}
}
+
+ fun bindNullable(icon: Icon?, view: ImageView) {
+ if (icon != null) {
+ view.visibility = View.VISIBLE
+ bind(icon, view)
+ } else {
+ view.visibility = View.GONE
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 88c3f9f6..e31f1ad 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -18,6 +18,7 @@
import android.provider.Settings
import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.CoreStartable
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
@@ -91,8 +92,8 @@
keyguardTransitionInteractor.startedKeyguardTransitionStep
.mapLatest(::determineSceneAfterTransition)
.filterNotNull()
- .onEach { nextScene ->
- communalSceneInteractor.changeScene(nextScene, CommunalTransitionKeys.SimpleFade)
+ .onEach { (nextScene, nextTransition) ->
+ communalSceneInteractor.changeScene(nextScene, nextTransition)
}
.launchIn(applicationScope)
@@ -188,7 +189,7 @@
private suspend fun determineSceneAfterTransition(
lastStartedTransition: TransitionStep,
- ): SceneKey? {
+ ): Pair<SceneKey, TransitionKey>? {
val to = lastStartedTransition.to
val from = lastStartedTransition.from
val docked = dockManager.isDocked
@@ -201,22 +202,27 @@
// underneath the hub is shown. When launching activities over lockscreen, we only
// change scenes once the activity launch animation is finished, so avoid
// changing the scene here.
- CommunalScenes.Blank
+ Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
}
to == KeyguardState.GLANCEABLE_HUB && from == KeyguardState.OCCLUDED -> {
// When transitioning to the hub from an occluded state, fade out the hub without
// doing any translation.
- CommunalScenes.Communal
+ Pair(CommunalScenes.Communal, CommunalTransitionKeys.SimpleFade)
}
// Transitioning to Blank scene when entering the edit mode will be handled separately
// with custom animations.
to == KeyguardState.GONE && !communalInteractor.editModeOpen.value ->
- CommunalScenes.Blank
+ Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
!docked && !KeyguardState.deviceIsAwakeInState(to) -> {
// If the user taps the screen and wakes the device within this timeout, we don't
// want to dismiss the hub
delay(AWAKE_DEBOUNCE_DELAY)
- CommunalScenes.Blank
+ Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
+ }
+ from == KeyguardState.DOZING && to == KeyguardState.GLANCEABLE_HUB -> {
+ // Make sure the communal hub is showing (immediately, not fading in) when
+ // transitioning from dozing to hub.
+ Pair(CommunalScenes.Communal, CommunalTransitionKeys.Immediately)
}
else -> null
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index cc90730..6ec6ec1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -129,7 +129,6 @@
/** Called as the UI requests opening the widget editor with an optional preselected widget. */
open fun onOpenWidgetEditor(
- preselectedKey: String? = null,
shouldOpenWidgetPickerOnStart: Boolean = false,
) {}
@@ -146,7 +145,7 @@
open fun onReorderWidgetCancel() {}
/** Called as the user request to show the customize widget button. */
- open fun onShowCustomizeWidgetButton() {}
+ open fun onLongClick() {}
/** Set the key of the currently selected item */
fun setSelectedKey(key: String?) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 11247df..2043dd1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -40,6 +40,7 @@
import com.android.systemui.media.dagger.MediaModule
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
@@ -76,9 +77,10 @@
@Main private val resources: Resources,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
keyguardInteractor: KeyguardInteractor,
+ private val keyguardIndicationController: KeyguardIndicationController,
communalSceneInteractor: CommunalSceneInteractor,
private val communalInteractor: CommunalInteractor,
- private val communalSettingsInteractor: CommunalSettingsInteractor,
+ communalSettingsInteractor: CommunalSettingsInteractor,
tutorialInteractor: CommunalTutorialInteractor,
private val shadeInteractor: ShadeInteractor,
@Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
@@ -218,9 +220,8 @@
}
override fun onOpenWidgetEditor(
- preselectedKey: String?,
shouldOpenWidgetPickerOnStart: Boolean,
- ) = communalInteractor.showWidgetEditor(preselectedKey, shouldOpenWidgetPickerOnStart)
+ ) = communalInteractor.showWidgetEditor(selectedKey.value, shouldOpenWidgetPickerOnStart)
override fun onDismissCtaTile() {
scope.launch {
@@ -229,7 +230,11 @@
}
}
- override fun onShowCustomizeWidgetButton() {
+ fun onClick() {
+ keyguardIndicationController.showActionToUnlock()
+ }
+
+ override fun onLongClick() {
setCurrentPopupType(PopupType.CustomizeWidgetButton)
}
@@ -319,7 +324,7 @@
}
sealed class PopupType {
- object CtaTile : PopupType()
+ data object CtaTile : PopupType()
- object CustomizeWidgetButton : PopupType()
+ data object CustomizeWidgetButton : PopupType()
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 10d6881..4c7142b 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -211,6 +211,9 @@
}
}
+ /** Whether the device is in lockdown mode, where bouncer input is required to unlock. */
+ val isInLockdown: Flow<Boolean> = deviceEntryRestrictionReason.map { it.isInLockdown() }
+
/**
* Attempt to enter the device and dismiss the lockscreen. If authentication is required to
* unlock the device it will transition to bouncer.
@@ -259,6 +262,27 @@
return repository.isLockscreenEnabled()
}
+ fun DeviceEntryRestrictionReason?.isInLockdown(): Boolean {
+ return when (this) {
+ DeviceEntryRestrictionReason.UserLockdown -> true
+ DeviceEntryRestrictionReason.PolicyLockdown -> true
+
+ // Add individual enum value instead of using "else" so new reasons are guaranteed
+ // to be added here at compile-time.
+ null -> false
+ DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> false
+ DeviceEntryRestrictionReason.BouncerLockedOut -> false
+ DeviceEntryRestrictionReason.AdaptiveAuthRequest -> false
+ DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> false
+ DeviceEntryRestrictionReason.TrustAgentDisabled -> false
+ DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> false
+ DeviceEntryRestrictionReason.SecurityTimeout -> false
+ DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> false
+ DeviceEntryRestrictionReason.UnattendedUpdate -> false
+ DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> false
+ }
+ }
+
/**
* Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically
* dismissed once the authentication challenge is completed. For example, completing a biometric
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
index ec92ca9..b9a5a228 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepository.kt
@@ -20,16 +20,24 @@
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.hardware.input.InputManager
import android.os.UserHandle
+import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
import com.android.systemui.CoreStartable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Inactive
+import com.android.systemui.shared.hardware.findInputDevice
import com.android.systemui.statusbar.CommandQueue
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
@SysUISingleton
class ShortcutHelperRepository
@@ -37,6 +45,9 @@
constructor(
private val commandQueue: CommandQueue,
private val broadcastDispatcher: BroadcastDispatcher,
+ private val inputManager: InputManager,
+ @Background private val backgroundScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : CoreStartable {
val state = MutableStateFlow<ShortcutHelperState>(Inactive)
@@ -44,7 +55,9 @@
override fun start() {
registerBroadcastReceiver(
action = Intent.ACTION_SHOW_KEYBOARD_SHORTCUTS,
- onReceive = { state.value = Active() }
+ onReceive = {
+ backgroundScope.launch { state.value = Active(findPhysicalKeyboardId()) }
+ }
)
registerBroadcastReceiver(
action = Intent.ACTION_DISMISS_KEYBOARD_SHORTCUTS,
@@ -72,6 +85,13 @@
)
}
+ private suspend fun findPhysicalKeyboardId() =
+ withContext(backgroundDispatcher) {
+ val firstEnabledPhysicalKeyboard =
+ inputManager.findInputDevice { it.isEnabled && it.isFullKeyboard && !it.isVirtual }
+ return@withContext firstEnabledPhysicalKeyboard?.id ?: VIRTUAL_KEYBOARD
+ }
+
fun hide() {
state.value = Inactive
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
index d22d6c8..1f22141 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutHelperState.kt
@@ -19,5 +19,5 @@
sealed interface ShortcutHelperState {
data object Inactive : ShortcutHelperState
- data class Active(val deviceId: Int? = null) : ShortcutHelperState
+ data class Active(val deviceId: Int) : ShortcutHelperState
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 209bc7a..c4b70d8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -89,6 +89,7 @@
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.power.shared.model.ScreenPowerState;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartable;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.scene.shared.model.Scenes;
import com.android.systemui.settings.DisplayTracker;
@@ -122,6 +123,7 @@
private final KeyguardInteractor mKeyguardInteractor;
private final Lazy<SceneInteractor> mSceneInteractorLazy;
private final Executor mMainExecutor;
+ private final Lazy<KeyguardStateCallbackStartable> mKeyguardStateCallbackStartableLazy;
private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers,
SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
@@ -341,7 +343,8 @@
Lazy<SceneInteractor> sceneInteractorLazy,
@Main Executor mainExecutor,
KeyguardInteractor keyguardInteractor,
- KeyguardEnabledInteractor keyguardEnabledInteractor) {
+ KeyguardEnabledInteractor keyguardEnabledInteractor,
+ Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy) {
super();
mKeyguardViewMediator = keyguardViewMediator;
mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -353,6 +356,7 @@
mKeyguardInteractor = keyguardInteractor;
mSceneInteractorLazy = sceneInteractorLazy;
mMainExecutor = mainExecutor;
+ mKeyguardStateCallbackStartableLazy = keyguardStateCallbackStartableLazy;
if (KeyguardWmStateRefactor.isEnabled()) {
WindowManagerLockscreenVisibilityViewBinder.bind(
@@ -440,7 +444,11 @@
public void addStateMonitorCallback(IKeyguardStateCallback callback) {
trace("addStateMonitorCallback");
checkPermission();
- mKeyguardViewMediator.addStateMonitorCallback(callback);
+ if (SceneContainerFlag.isEnabled()) {
+ mKeyguardStateCallbackStartableLazy.get().addCallback(callback);
+ } else {
+ mKeyguardViewMediator.addStateMonitorCallback(callback);
+ }
}
@Override // Binder interface
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index b70dbe2..36b7ed2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -3801,6 +3801,10 @@
}
private void notifyDefaultDisplayCallbacks(boolean showing) {
+ if (SceneContainerFlag.isEnabled()) {
+ return;
+ }
+
// TODO(b/140053364)
whitelistIpcs(() -> {
int size = mKeyguardStateCallbacks.size();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index cdf3b06..888d047 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -39,6 +39,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardDone
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.time.SystemClock
@@ -293,6 +294,15 @@
/** Sets whether the keyguard is enabled (see [isKeyguardEnabled]). */
fun setKeyguardEnabled(enabled: Boolean)
+
+ /** @see isShowKeyguardWhenReenabled */
+ fun setShowKeyguardWhenReenabled(isShowKeyguardWhenReenabled: Boolean)
+
+ /**
+ * Returns `true` if the keyguard should be re-shown once it becomes re-enabled again; `false`
+ * otherwise.
+ */
+ fun isShowKeyguardWhenReenabled(): Boolean
}
/** Encapsulates application state for the keyguard. */
@@ -474,6 +484,8 @@
private val _isDozing = MutableStateFlow(statusBarStateController.isDozing)
override val isDozing: StateFlow<Boolean> = _isDozing.asStateFlow()
+ private var isShowKeyguardWhenReenabled: Boolean = false
+
override fun setIsDozing(isDozing: Boolean) {
_isDozing.value = isDozing
}
@@ -692,6 +704,16 @@
_isKeyguardEnabled.value = enabled
}
+ override fun setShowKeyguardWhenReenabled(isShowKeyguardWhenReenabled: Boolean) {
+ SceneContainerFlag.assertInNewMode()
+ this.isShowKeyguardWhenReenabled = isShowKeyguardWhenReenabled
+ }
+
+ override fun isShowKeyguardWhenReenabled(): Boolean {
+ SceneContainerFlag.assertInNewMode()
+ return isShowKeyguardWhenReenabled
+ }
+
private fun statusBarStateIntToObject(value: Int): StatusBarState {
return when (value) {
0 -> StatusBarState.SHADE
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
index 6522439..bd5d096 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/TrustRepository.kt
@@ -23,11 +23,13 @@
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.shared.model.ActiveUnlockModel
import com.android.systemui.keyguard.shared.model.TrustManagedModel
import com.android.systemui.keyguard.shared.model.TrustModel
import com.android.systemui.user.data.repository.UserRepository
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -44,6 +46,7 @@
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
/** Encapsulates any state relevant to trust agents and trust grants. */
interface TrustRepository {
@@ -51,7 +54,7 @@
val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
/** Flow representing whether the current user is trusted. */
- val isCurrentUserTrusted: Flow<Boolean>
+ val isCurrentUserTrusted: StateFlow<Boolean>
/** Flow representing whether active unlock is running for the current user. */
val isCurrentUserActiveUnlockRunning: Flow<Boolean>
@@ -63,6 +66,9 @@
/** A trust agent is requesting to dismiss the keyguard from a trust change. */
val trustAgentRequestingToDismissKeyguard: Flow<TrustModel>
+
+ /** Reports a keyguard visibility change. */
+ suspend fun reportKeyguardShowingChanged()
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -71,6 +77,7 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
private val userRepository: UserRepository,
private val trustManager: TrustManager,
private val logger: TrustRepositoryLogger,
@@ -191,11 +198,26 @@
private fun isUserTrustManaged(userId: Int) =
trustManagedForUser[userId]?.isTrustManaged ?: false
- override val isCurrentUserTrusted: Flow<Boolean>
+ override val isCurrentUserTrusted: StateFlow<Boolean>
get() =
combine(trust, userRepository.selectedUserInfo, ::Pair)
- .map { latestTrustModelForUser[it.second.id]?.isTrusted ?: false }
+ .map { isCurrentUserTrusted(it.second.id) }
.distinctUntilChanged()
.onEach { logger.isCurrentUserTrusted(it) }
.onStart { emit(false) }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = isCurrentUserTrusted(),
+ )
+
+ private fun isCurrentUserTrusted(
+ selectedUserId: Int = userRepository.getSelectedUserInfo().id
+ ): Boolean {
+ return latestTrustModelForUser[selectedUserId]?.isTrusted ?: false
+ }
+
+ override suspend fun reportKeyguardShowingChanged() {
+ withContext(backgroundDispatcher) { trustManager.reportKeyguardShowingChanged() }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index f3692bd..506a18d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -17,8 +17,10 @@
package com.android.systemui.keyguard.domain.interactor
import android.animation.ValueAnimator
+import android.app.DreamManager
import com.android.app.animation.Interpolators
import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -51,8 +53,10 @@
keyguardInteractor: KeyguardInteractor,
powerInteractor: PowerInteractor,
private val communalInteractor: CommunalInteractor,
+ private val communalSceneInteractor: CommunalSceneInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
val deviceEntryRepository: DeviceEntryRepository,
+ private val dreamManager: DreamManager,
) :
TransitionInteractor(
fromState = KeyguardState.DOZING,
@@ -119,7 +123,8 @@
.filterRelevantKeyguardStateAnd { isAwake -> isAwake }
.sample(
keyguardInteractor.isKeyguardOccluded,
- communalInteractor.isIdleOnCommunal,
+ communalInteractor.isCommunalAvailable,
+ communalSceneInteractor.isIdleOnCommunal,
canTransitionToGoneOnWake,
keyguardInteractor.primaryBouncerShowing,
)
@@ -127,6 +132,7 @@
(
_,
occluded,
+ isCommunalAvailable,
isIdleOnCommunal,
canTransitionToGoneOnWake,
primaryBouncerShowing) ->
@@ -141,6 +147,10 @@
KeyguardState.OCCLUDED
} else if (isIdleOnCommunal) {
KeyguardState.GLANCEABLE_HUB
+ } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+ // This case handles tapping the power button to transition through
+ // dream -> off -> hub.
+ KeyguardState.GLANCEABLE_HUB
} else {
KeyguardState.LOCKSCREEN
}
@@ -159,7 +169,8 @@
powerInteractor.detailedWakefulness
.filterRelevantKeyguardStateAnd { it.isAwake() }
.sample(
- communalInteractor.isIdleOnCommunal,
+ communalInteractor.isCommunalAvailable,
+ communalSceneInteractor.isIdleOnCommunal,
keyguardInteractor.biometricUnlockState,
canTransitionToGoneOnWake,
keyguardInteractor.primaryBouncerShowing,
@@ -167,6 +178,7 @@
.collect {
(
_,
+ isCommunalAvailable,
isIdleOnCommunal,
biometricUnlockState,
canDismissLockscreen,
@@ -188,6 +200,10 @@
KeyguardState.PRIMARY_BOUNCER
} else if (isIdleOnCommunal) {
KeyguardState.GLANCEABLE_HUB
+ } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+ // This case handles tapping the power button to transition through
+ // dream -> off -> hub.
+ KeyguardState.GLANCEABLE_HUB
} else {
KeyguardState.LOCKSCREEN
},
@@ -216,5 +232,6 @@
val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
val TO_GONE_DURATION = DEFAULT_DURATION
val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION
+ val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index f5b12a2..07a2b04 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -99,19 +99,21 @@
}
}
- scope.launch {
- keyguardRepository.isKeyguardEnabled
- .filterRelevantKeyguardStateAnd { enabled -> enabled }
- .sample(keyguardEnabledInteractor.showKeyguardWhenReenabled)
- .filter { reshow -> reshow }
- .collect {
- startTransitionTo(
- KeyguardState.LOCKSCREEN,
- ownerReason =
- "Keyguard was re-enabled, and we weren't GONE when it " +
- "was originally disabled"
- )
- }
+ if (!SceneContainerFlag.isEnabled) {
+ scope.launch {
+ keyguardRepository.isKeyguardEnabled
+ .filterRelevantKeyguardStateAnd { enabled -> enabled }
+ .sample(keyguardEnabledInteractor.showKeyguardWhenReenabled)
+ .filter { reshow -> reshow }
+ .collect {
+ startTransitionTo(
+ KeyguardState.LOCKSCREEN,
+ ownerReason =
+ "Keyguard was re-enabled, and we weren't GONE when it " +
+ "was originally disabled"
+ )
+ }
+ }
}
} else {
scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index f8208b3..f23b12f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -233,6 +233,7 @@
KeyguardState.DOZING -> TO_DOZING_DURATION
KeyguardState.GONE -> TO_GONE_DURATION
KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
+ KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
else -> DEFAULT_DURATION
}.inWholeMilliseconds
}
@@ -245,6 +246,7 @@
val TO_GONE_DURATION = 500.milliseconds
val TO_GONE_SHORT_DURATION = 200.milliseconds
val TO_LOCKSCREEN_DURATION = 450.milliseconds
+ val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
val TO_GONE_SURFACE_BEHIND_VISIBLE_THRESHOLD = 0.5f
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
index 8dede01..8d683f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
@@ -21,12 +21,15 @@
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
/**
@@ -48,6 +51,38 @@
transitionInteractor: KeyguardTransitionInteractor,
) {
+ /**
+ * Whether the keyguard is enabled, per [KeyguardService]. If the keyguard is not enabled, the
+ * lockscreen cannot be shown and the device will go from AOD/DOZING directly to GONE.
+ *
+ * Keyguard can be disabled by selecting Security: "None" in settings, or by apps that hold
+ * permission to do so (such as Phone).
+ *
+ * If the keyguard is disabled while we're locked, we will transition to GONE unless we're in
+ * lockdown mode. If the keyguard is re-enabled, we'll transition back to LOCKSCREEN if we were
+ * locked when it was disabled.
+ */
+ val isKeyguardEnabled: StateFlow<Boolean> = repository.isKeyguardEnabled
+
+ /**
+ * Whether we need to show the keyguard when the keyguard is re-enabled, since we hid it when it
+ * became disabled.
+ */
+ val showKeyguardWhenReenabled: Flow<Boolean> =
+ repository.isKeyguardEnabled
+ .onEach { SceneContainerFlag.assertInLegacyMode() }
+ // Whenever the keyguard is disabled...
+ .filter { enabled -> !enabled }
+ .sampleCombine(
+ transitionInteractor.currentTransitionInfoInternal,
+ biometricSettingsRepository.isCurrentUserInLockdown
+ )
+ .map { (_, transitionInfo, inLockdown) ->
+ // ...we hide the keyguard, if it's showing and we're not in lockdown. In that case,
+ // we want to remember that and re-show it when keyguard is enabled again.
+ transitionInfo.to != KeyguardState.GONE && !inLockdown
+ }
+
init {
/**
* Whenever keyguard is disabled, transition to GONE unless we're in lockdown or already
@@ -68,25 +103,15 @@
}
}
- /**
- * Whether we need to show the keyguard when the keyguard is re-enabled, since we hid it when it
- * became disabled.
- */
- val showKeyguardWhenReenabled: Flow<Boolean> =
- repository.isKeyguardEnabled
- // Whenever the keyguard is disabled...
- .filter { enabled -> !enabled }
- .sampleCombine(
- transitionInteractor.currentTransitionInfoInternal,
- biometricSettingsRepository.isCurrentUserInLockdown
- )
- .map { (_, transitionInfo, inLockdown) ->
- // ...we hide the keyguard, if it's showing and we're not in lockdown. In that case,
- // we want to remember that and re-show it when keyguard is enabled again.
- transitionInfo.to != KeyguardState.GONE && !inLockdown
- }
-
fun notifyKeyguardEnabled(enabled: Boolean) {
repository.setKeyguardEnabled(enabled)
}
+
+ fun setShowKeyguardWhenReenabled(isShowKeyguardWhenReenabled: Boolean) {
+ repository.setShowKeyguardWhenReenabled(isShowKeyguardWhenReenabled)
+ }
+
+ fun isShowKeyguardWhenReenabled(): Boolean {
+ return repository.isShowKeyguardWhenReenabled()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt
index 2ff6e16..73248bb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TrustInteractor.kt
@@ -17,14 +17,21 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.TrustRepository
import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
/** Encapsulates any state relevant to trust agents and trust grants. */
@SysUISingleton
-class TrustInteractor @Inject constructor(repository: TrustRepository) {
+class TrustInteractor
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val repository: TrustRepository,
+) {
/**
* Whether the current user has a trust agent enabled. This is true if the user has at least one
* trust agent enabled in settings.
@@ -39,5 +46,10 @@
val isTrustAgentCurrentlyAllowed: StateFlow<Boolean> = repository.isCurrentUserTrustManaged
/** Whether the current user is trusted by any of the enabled trust agents. */
- val isTrusted: Flow<Boolean> = repository.isCurrentUserTrusted
+ val isTrusted: StateFlow<Boolean> = repository.isCurrentUserTrusted
+
+ /** Reports a keyguard visibility change. */
+ fun reportKeyguardShowingChanged() {
+ applicationScope.launch { repository.reportKeyguardShowingChanged() }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 3e4253b..ba94f45 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -133,6 +133,12 @@
configurationBasedDimensions.value = loadFromResources(view)
}
}
+
+ launch("$TAG#viewModel.visible") {
+ viewModel.visible.collect { visible ->
+ indicationController.setVisible(visible)
+ }
+ }
}
}
return disposables
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
index ecdc21c..dc7a649 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt
@@ -24,6 +24,7 @@
import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToGoneTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
@@ -49,6 +50,7 @@
import com.android.systemui.keyguard.ui.viewmodel.OffToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToDozingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToLockscreenTransitionViewModel
import dagger.Binds
import dagger.Module
@@ -253,4 +255,16 @@
abstract fun goneToGlanceableHub(
impl: GoneToGlanceableHubTransitionViewModel
): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
+ abstract fun primaryBouncerToGlanceableHub(
+ impl: PrimaryBouncerToGlanceableHubTransitionViewModel
+ ): DeviceEntryIconTransition
+
+ @Binds
+ @IntoSet
+ abstract fun dozingToGlanceableHub(
+ impl: DozingToGlanceableHubTransitionViewModel
+ ): DeviceEntryIconTransition
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt
new file mode 100644
index 0000000..aee34e1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToGlanceableHubTransitionViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDozingTransitionInteractor.Companion.TO_GLANCEABLE_HUB_DURATION
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.scene.shared.model.Scenes
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class DozingToGlanceableHubTransitionViewModel
+@Inject
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+ private val transitionAnimation =
+ animationFlow
+ .setup(
+ duration = TO_GLANCEABLE_HUB_DURATION,
+ edge = Edge.create(DOZING, Scenes.Communal)
+ )
+ .setupWithoutSceneContainer(edge = Edge.create(DOZING, GLANCEABLE_HUB))
+
+ override val deviceEntryParentViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index a758720d..609b571d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.doze.util.BurnInHelperWrapper
@@ -28,9 +29,10 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.BurnInModel
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -44,14 +46,15 @@
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
- private val bottomAreaInteractor: KeyguardBottomAreaInteractor,
+ bottomAreaInteractor: KeyguardBottomAreaInteractor,
keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel,
private val burnInHelperWrapper: BurnInHelperWrapper,
- private val burnInInteractor: BurnInInteractor,
- private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
+ burnInInteractor: BurnInInteractor,
+ shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
configurationInteractor: ConfigurationInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
- @Background private val backgroundCoroutineContext: CoroutineContext,
+ communalSceneInteractor: CommunalSceneInteractor,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
@Main private val mainDispatcher: CoroutineDispatcher,
) {
@@ -61,6 +64,13 @@
/** An observable for the alpha level for the entire bottom area. */
val alpha: Flow<Float> = keyguardBottomAreaViewModel.alpha
+ /** An observable for the visibility value for the indication area view. */
+ val visible: Flow<Boolean> =
+ anyOf(
+ keyguardInteractor.statusBarState.map { state -> state == StatusBarState.KEYGUARD },
+ communalSceneInteractor.isCommunalVisible
+ )
+
/** An observable for whether the indication area should be padded. */
val isIndicationAreaPadded: Flow<Boolean> =
if (KeyguardBottomAreaRefactor.isEnabled) {
@@ -97,7 +107,7 @@
)
}
.distinctUntilChanged()
- .flowOn(backgroundCoroutineContext)
+ .flowOn(backgroundDispatcher)
/** An observable for the x-offset by which the indication area should be translated. */
val indicationAreaTranslationX: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
new file mode 100644
index 0000000..754fb94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor.Companion.TO_GLANCEABLE_HUB_DURATION
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
+import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.scene.shared.model.Scenes
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class PrimaryBouncerToGlanceableHubTransitionViewModel
+@Inject
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
+ private val transitionAnimation =
+ animationFlow
+ .setup(
+ duration = TO_GLANCEABLE_HUB_DURATION,
+ edge = Edge.create(PRIMARY_BOUNCER, Scenes.Communal)
+ )
+ .setupWithoutSceneContainer(edge = Edge.create(PRIMARY_BOUNCER, GLANCEABLE_HUB))
+
+ override val deviceEntryParentViewAlpha: Flow<Float> =
+ transitionAnimation.immediatelyTransitionTo(1f)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index a2d7fb1..e8a2334 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -27,6 +27,7 @@
import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
import com.android.systemui.media.controls.util.MediaSmartspaceLogger
+import com.android.systemui.media.controls.util.MediaSmartspaceLogger.Companion.SMARTSPACE_CARD_DISMISS_EVENT
import com.android.systemui.media.controls.util.SmallHash
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.time.SystemClock
@@ -362,6 +363,77 @@
return _smartspaceMediaData.value.isActive
}
+ /** Log user event on media card if smartspace logging is enabled. */
+ fun logSmartspaceCardUserEvent(
+ eventId: Int,
+ location: Int,
+ interactedSubCardRank: Int = 0,
+ interactedSubCardCardinality: Int = 0,
+ instanceId: InstanceId? = null,
+ isRec: Boolean = false
+ ) {
+ _currentMedia.value.forEachIndexed { index, mediaCommonModel ->
+ when (mediaCommonModel) {
+ is MediaCommonModel.MediaControl -> {
+ if (mediaCommonModel.mediaLoadedModel.instanceId == instanceId) {
+ if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+ logSmartspaceMediaCardUserEvent(
+ instanceId,
+ index,
+ eventId,
+ location,
+ mediaCommonModel.mediaLoadedModel.isSsReactivated,
+ interactedSubCardRank,
+ interactedSubCardCardinality
+ )
+ }
+ return
+ }
+ }
+ is MediaCommonModel.MediaRecommendations -> {
+ if (isRec) {
+ if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+ logSmarspaceRecommendationCardUserEvent(
+ eventId,
+ location,
+ index,
+ interactedSubCardRank,
+ interactedSubCardCardinality
+ )
+ }
+ return
+ }
+ }
+ }
+ }
+ }
+
+ /** Log media and recommendation cards dismissal if smartspace logging is enabled for each. */
+ fun logSmartspaceCardsOnSwipeToDismiss(location: Int) {
+ _currentMedia.value.forEachIndexed { index, mediaCommonModel ->
+ if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+ when (mediaCommonModel) {
+ is MediaCommonModel.MediaControl ->
+ logSmartspaceMediaCardUserEvent(
+ mediaCommonModel.mediaLoadedModel.instanceId,
+ index,
+ SMARTSPACE_CARD_DISMISS_EVENT,
+ location,
+ mediaCommonModel.mediaLoadedModel.isSsReactivated,
+ isSwipeToDismiss = true
+ )
+ is MediaCommonModel.MediaRecommendations ->
+ logSmarspaceRecommendationCardUserEvent(
+ SMARTSPACE_CARD_DISMISS_EVENT,
+ location,
+ index,
+ isSwipeToDismiss = true
+ )
+ }
+ }
+ }
+ }
+
private fun canBeRemoved(data: MediaData): Boolean {
return data.isPlaying?.let { !it } ?: data.isClearable && !data.active
}
@@ -394,6 +466,54 @@
}
}
+ private fun logSmartspaceMediaCardUserEvent(
+ instanceId: InstanceId,
+ index: Int,
+ eventId: Int,
+ location: Int,
+ isReactivated: Boolean,
+ interactedSubCardRank: Int = 0,
+ interactedSubCardCardinality: Int = 0,
+ isSwipeToDismiss: Boolean = false
+ ) {
+ _selectedUserEntries.value[instanceId]?.let {
+ smartspaceLogger.logSmartspaceCardUIEvent(
+ eventId,
+ it.smartspaceId,
+ it.appUid,
+ location,
+ _currentMedia.value.size,
+ isSsReactivated = isReactivated,
+ interactedSubcardRank = interactedSubCardRank,
+ interactedSubcardCardinality = interactedSubCardCardinality,
+ rank = index,
+ isSwipeToDismiss = isSwipeToDismiss,
+ )
+ }
+ }
+
+ private fun logSmarspaceRecommendationCardUserEvent(
+ eventId: Int,
+ location: Int,
+ index: Int,
+ interactedSubCardRank: Int = 0,
+ interactedSubCardCardinality: Int = 0,
+ isSwipeToDismiss: Boolean = false
+ ) {
+ smartspaceLogger.logSmartspaceCardUIEvent(
+ eventId,
+ SmallHash.hash(_smartspaceMediaData.value.targetId),
+ _smartspaceMediaData.value.getUid(applicationContext),
+ location,
+ _currentMedia.value.size,
+ isRecommendationCard = true,
+ interactedSubcardRank = interactedSubCardRank,
+ interactedSubcardCardinality = interactedSubCardCardinality,
+ rank = index,
+ isSwipeToDismiss = isSwipeToDismiss,
+ )
+ }
+
private fun isSmartspaceLoggingEnabled(commonModel: MediaCommonModel, index: Int): Boolean {
return sortedMedia.size > index &&
(_smartspaceMediaData.value.expiryTimeMs != 0L ||
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 5ec4f88..846c596 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -19,6 +19,7 @@
import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
+import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation;
import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
import android.animation.Animator;
@@ -577,13 +578,23 @@
&& mActivityIntentHelper.wouldPendingShowOverLockscreen(clickIntent,
mLockscreenUserManager.getCurrentUserId());
if (showOverLockscreen) {
- try {
- ActivityOptions opts = ActivityOptions.makeBasic();
- opts.setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
- clickIntent.send(opts.toBundle());
- } catch (PendingIntent.CanceledException e) {
- Log.e(TAG, "Pending intent for " + key + " was cancelled");
+ if (mediaLockscreenLaunchAnimation()) {
+ mActivityStarter.startPendingIntentMaybeDismissingKeyguard(
+ clickIntent,
+ /* dismissShade = */ true,
+ /* intentSentUiThreadCallback = */ null,
+ buildLaunchAnimatorController(mMediaViewHolder.getPlayer()),
+ /* fillIntent = */ null,
+ /* extraOptions = */ null);
+ } else {
+ try {
+ ActivityOptions opts = ActivityOptions.makeBasic();
+ opts.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ clickIntent.send(opts.toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Pending intent for " + key + " was cancelled");
+ }
}
} else {
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
index 01fbf4a..d1184b6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
@@ -18,6 +18,8 @@
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shared.system.SysUiStatsLog
import javax.inject.Inject
@@ -85,6 +87,8 @@
cardinality: Int,
isRecommendationCard: Boolean = false,
isSsReactivated: Boolean = false,
+ interactedSubcardRank: Int = 0,
+ interactedSubcardCardinality: Int = 0,
rank: Int = 0,
isSwipeToDismiss: Boolean = false,
) {
@@ -96,6 +100,8 @@
cardinality,
isRecommendationCard,
isSsReactivated,
+ interactedSubcardRank,
+ interactedSubcardCardinality,
rank = rank,
isSwipeToDismiss = isSwipeToDismiss,
)
@@ -187,5 +193,27 @@
const val SMARTSPACE_CARD_CLICK_EVENT = 760
const val SMARTSPACE_CARD_DISMISS_EVENT = 761
const val SMARTSPACE_CARD_SEEN_EVENT = 800
+
+ /**
+ * Get the location of media view given [currentEndLocation]
+ *
+ * @return location used for Smartspace logging
+ */
+ fun getSurface(location: Int): Int {
+ SceneContainerFlag.isUnexpectedlyInLegacyMode()
+ return when (location) {
+ MediaHierarchyManager.LOCATION_QQS,
+ MediaHierarchyManager.LOCATION_QS -> {
+ SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE
+ }
+ MediaHierarchyManager.LOCATION_LOCKSCREEN -> {
+ SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN
+ }
+ MediaHierarchyManager.LOCATION_DREAM_OVERLAY -> {
+ SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY
+ }
+ else -> SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
index 80c4379..2e5ff9d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java
@@ -56,6 +56,8 @@
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
import com.android.internal.accessibility.common.ShortcutConstants;
import com.android.systemui.Dumpable;
@@ -64,12 +66,14 @@
import com.android.systemui.accessibility.SystemActions;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.settings.DisplayTracker;
import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shared.rotation.RotationPolicyUtil;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -108,6 +112,7 @@
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Executor mMainExecutor;
+ private final Handler mBgHandler;
private final AccessibilityManager mAccessibilityManager;
private final Lazy<AssistManager> mAssistManagerLazy;
private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@@ -160,13 +165,15 @@
// Listens for changes to display rotation
private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() {
+ @WorkerThread
@Override
public void onRotationChanged(final int rotation) {
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
+ @Nullable Boolean isRotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
mHandler.postAtFrontOfQueue(() -> {
mRotationWatcherRotation = rotation;
- dispatchRotationChanged(rotation);
+ dispatchRotationChanged(rotation, isRotationLocked);
});
}
};
@@ -194,7 +201,8 @@
ConfigurationController configurationController,
DumpManager dumpManager,
CommandQueue commandQueue,
- @Main Executor mainExecutor) {
+ @Main Executor mainExecutor,
+ @Background Handler bgHandler) {
// b/319489709: This component shouldn't be running for a non-primary user
if (!Process.myUserHandle().equals(UserHandle.SYSTEM)) {
Log.wtf(TAG, "Unexpected initialization for non-primary user", new Throwable());
@@ -215,6 +223,7 @@
mDefaultDisplayId = displayTracker.getDefaultDisplayId();
mEdgeBackGestureHandler = edgeBackGestureHandlerFactory.create(context);
mMainExecutor = mainExecutor;
+ mBgHandler = bgHandler;
mNavBarMode = navigationModeController.addListener(this);
mCommandQueue.addCallback(this);
@@ -322,7 +331,13 @@
listener.updateAssistantAvailable(mAssistantAvailable, mLongPressHomeEnabled);
}
listener.updateWallpaperVisibility(mWallpaperVisible, mDefaultDisplayId);
- listener.updateRotationWatcherState(mRotationWatcherRotation);
+
+ mBgHandler.post(() -> {
+ Boolean isRotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
+ mMainExecutor.execute(
+ () -> listener.updateRotationWatcherState(
+ mRotationWatcherRotation, isRotationLocked));
+ });
}
/**
@@ -526,9 +541,9 @@
}
}
- private void dispatchRotationChanged(int rotation) {
+ private void dispatchRotationChanged(int rotation, @Nullable Boolean isRotationLocked) {
for (NavbarTaskbarStateUpdater listener : mStateListeners) {
- listener.updateRotationWatcherState(rotation);
+ listener.updateRotationWatcherState(rotation, isRotationLocked);
}
}
@@ -544,7 +559,7 @@
void updateAccessibilityServicesState();
void updateAssistantAvailable(boolean available, boolean longPressHomeEnabled);
default void updateWallpaperVisibility(boolean visible, int displayId) {}
- default void updateRotationWatcherState(int rotation) {}
+ default void updateRotationWatcherState(int rotation, @Nullable Boolean isRotationLocked) {}
}
/** Data class to help Taskbar/Navbar initiate state correctly when switching between the two.*/
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 07289cb..69aa450 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -136,6 +136,7 @@
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.shared.rotation.RotationButtonController;
+import com.android.systemui.shared.rotation.RotationPolicyUtil;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.shared.system.TaskStackChangeListener;
@@ -366,9 +367,11 @@
}
@Override
- public void updateRotationWatcherState(int rotation) {
+ public void updateRotationWatcherState(
+ int rotation, @Nullable Boolean isRotationLocked) {
if (mIsOnDefaultDisplay && mView != null) {
- mView.getRotationButtonController().onRotationWatcherChanged(rotation);
+ RotationButtonController controller = mView.getRotationButtonController();
+ controller.onRotationWatcherChanged(rotation, isRotationLocked);
if (mView.needsReorient(rotation)) {
repositionNavigationBar(rotation);
}
@@ -819,8 +822,9 @@
// Reset user rotation pref to match that of the WindowManager if starting in locked
// mode. This will automatically happen when switching from auto-rotate to locked mode.
- if (display != null && rotationButtonController.isRotationLocked()) {
- rotationButtonController.setRotationLockedAtAngle(
+ @Nullable Boolean isRotationLocked = RotationPolicyUtil.isRotationLocked(mContext);
+ if (display != null && isRotationLocked) {
+ rotationButtonController.setRotationLockedAtAngle(isRotationLocked,
display.getRotation(), /* caller= */ "NavigationBar#onViewAttached");
}
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index b177b0b..1c2a087 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -23,6 +23,7 @@
import static com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler.DEBUG_MISSING_GESTURE_TAG;
import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -285,8 +286,10 @@
@VisibleForTesting
boolean supportsTaskbar() {
- // Enable for tablets, unfolded state on a foldable device or (non handheld AND flag is set)
- return mIsLargeScreen || (!mIsPhone && enableTaskbarNavbarUnification());
+ // Enable for tablets, unfolded state on a foldable device, (non handheld AND flag is set),
+ // or handheld when enableTaskbarOnPhones() returns true.
+ boolean foldedOrPhone = !mIsPhone || enableTaskbarOnPhones();
+ return mIsLargeScreen || (foldedOrPhone && enableTaskbarNavbarUnification());
}
private final CommandQueue.Callbacks mCommandQueueCallbacks = new CommandQueue.Callbacks() {
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
index d0c7fbc..1f74716 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
@@ -22,6 +22,8 @@
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeAlignment
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -29,11 +31,19 @@
/** Models UI state and handles user input for the Notifications Shade scene. */
@SysUISingleton
-class NotificationsShadeSceneViewModel @Inject constructor() {
+class NotificationsShadeSceneViewModel
+@Inject
+constructor(
+ shadeInteractor: ShadeInteractor,
+) {
val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
MutableStateFlow(
mapOf(
- Swipe.Up to SceneFamilies.Home,
+ if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) {
+ Swipe.Up
+ } else {
+ Swipe.Down
+ } to SceneFamilies.Home,
Back to SceneFamilies.Home,
)
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 24b7a01..96df728 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -224,6 +224,10 @@
});
}
+ boolean isBound() {
+ return mBound.get();
+ }
+
@WorkerThread
private void setBindService(boolean bind) {
if (mBound.get() && mUnbindImmediate.get()) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index 6bc5095..d10471d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -217,7 +217,11 @@
Log.e(TAG, "Service already bound");
return;
}
- mPendingBind = true;
+ if (!mStateManager.isBound()) {
+ // If we are bound, we don't need to set a pending bind. There's either one already or
+ // we are fully bound.
+ mPendingBind = true;
+ }
mBound = true;
mJustBound = true;
mHandler.postDelayed(mJustBoundOver, MIN_BIND_TIME);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
index d8af3fa..0285cbd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.data.repository
+package com.android.systemui.qs.panels.dagger
-import com.android.systemui.kosmos.Kosmos
+import javax.inject.Qualifier
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class PaginatedBaseLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 7b67993..c214361 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -30,18 +30,21 @@
import com.android.systemui.qs.panels.shared.model.GridLayoutType
import com.android.systemui.qs.panels.shared.model.IconLabelVisibilityLog
import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
+import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
import com.android.systemui.qs.panels.shared.model.StretchedGridLayoutType
import com.android.systemui.qs.panels.ui.compose.GridLayout
import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout
import com.android.systemui.qs.panels.ui.compose.PartitionedGridLayout
import com.android.systemui.qs.panels.ui.compose.StretchedGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModelImpl
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModelImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -62,14 +65,24 @@
@Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
- @Binds fun bindGridSizeViewModel(impl: InfiniteGridSizeViewModelImpl): InfiniteGridSizeViewModel
+ @Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel
@Binds
fun bindIconLabelVisibilityViewModel(
impl: IconLabelVisibilityViewModelImpl
): IconLabelVisibilityViewModel
- @Binds @Named("Default") fun bindDefaultGridLayout(impl: PartitionedGridLayout): GridLayout
+ @Binds
+ @PaginatedBaseLayoutType
+ fun bindPaginatedBaseGridLayout(impl: PartitionedGridLayout): PaginatableGridLayout
+
+ @Binds
+ @PaginatedBaseLayoutType
+ fun bindPaginatedBaseConsistencyInteractor(
+ impl: NoopGridConsistencyInteractor
+ ): GridTypeConsistencyInteractor
+
+ @Binds @Named("Default") fun bindDefaultGridLayout(impl: PaginatedGridLayout): GridLayout
companion object {
@Provides
@@ -109,6 +122,14 @@
}
@Provides
+ @IntoSet
+ fun providePaginatedGridLayout(
+ gridLayout: PaginatedGridLayout
+ ): Pair<GridLayoutType, GridLayout> {
+ return Pair(PaginatedGridLayoutType, gridLayout)
+ }
+
+ @Provides
fun provideGridLayoutMap(
entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridLayout>>
): Map<GridLayoutType, GridLayout> {
@@ -147,6 +168,14 @@
}
@Provides
+ @IntoSet
+ fun providePaginatedGridConsistencyInteractor(
+ @PaginatedBaseLayoutType consistencyInteractor: GridTypeConsistencyInteractor,
+ ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
+ return Pair(PaginatedGridLayoutType, consistencyInteractor)
+ }
+
+ @Provides
fun provideGridConsistencyInteractorMap(
entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>>
): Map<GridLayoutType, GridTypeConsistencyInteractor> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
index 43ccdf66..32ce973 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
@@ -23,7 +23,7 @@
import kotlinx.coroutines.flow.asStateFlow
@SysUISingleton
-class InfiniteGridSizeRepository @Inject constructor() {
+class FixedColumnsRepository @Inject constructor() {
// Number of columns in the narrowest state for consistency
private val _columns = MutableStateFlow(4)
val columns: StateFlow<Int> = _columns.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
index 44d8688..47c4ffd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
@@ -18,7 +18,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
+import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -26,13 +26,14 @@
interface GridLayoutTypeRepository {
val layout: StateFlow<GridLayoutType>
+
fun setLayout(type: GridLayoutType)
}
@SysUISingleton
class GridLayoutTypeRepositoryImpl @Inject constructor() : GridLayoutTypeRepository {
private val _layout: MutableStateFlow<GridLayoutType> =
- MutableStateFlow(PartitionedGridLayoutType)
+ MutableStateFlow(PaginatedGridLayoutType)
override val layout = _layout.asStateFlow()
override fun setLayout(type: GridLayoutType) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt
new file mode 100644
index 0000000..26b2e2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.emitOnStart
+import javax.inject.Inject
+import kotlinx.coroutines.flow.map
+
+/**
+ * Provides the number of [rows] to use with a paginated grid, by tracking the resource
+ * [R.integer.quick_settings_max_rows].
+ */
+@SysUISingleton
+class PaginatedGridRepository
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ configurationRepository: ConfigurationRepository,
+) {
+ val rows =
+ configurationRepository.onConfigurationChange.emitOnStart().map {
+ resources.getInteger(R.integer.quick_settings_max_rows)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
index 13c6072..9591002 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
@@ -17,11 +17,11 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.data.repository.InfiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.FixedColumnsRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
@SysUISingleton
-class InfiniteGridSizeInteractor @Inject constructor(repo: InfiniteGridSizeRepository) {
+class FixedColumnsSizeInteractor @Inject constructor(repo: FixedColumnsRepository) {
val columns: StateFlow<Int> = repo.columns
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
index e99c64c..0fe79af 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
@@ -28,7 +28,7 @@
@Inject
constructor(
private val iconTilesInteractor: IconTilesInteractor,
- private val gridSizeInteractor: InfiniteGridSizeInteractor
+ private val gridSizeInteractor: FixedColumnsSizeInteractor
) : GridTypeConsistencyInteractor {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
index 97ceacc..d7d1ce9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
@@ -17,10 +17,14 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.panels.data.repository.PaginatedGridRepository
import javax.inject.Inject
@SysUISingleton
-class NoopConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor {
- override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles
+class PaginatedGridInteractor
+@Inject
+constructor(paginatedGridRepository: PaginatedGridRepository) {
+ val rows = paginatedGridRepository.rows
+
+ val defaultRows = 4
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
index 9550ddb..b1942fe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
@@ -34,3 +34,6 @@
/** Grid type grouping large tiles on top and icon tiles at the bottom. */
data object PartitionedGridLayoutType : GridLayoutType
+
+/** Grid type for a paginated list of tiles. It will delegate to some other layout type. */
+data object PaginatedGridLayoutType : GridLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 8806931..e2f6bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,15 +18,19 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
+/** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */
interface GridLayout {
@Composable
fun TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
+ editModeStart: () -> Unit,
)
@Composable
@@ -37,3 +41,49 @@
onRemoveTile: (TileSpec) -> Unit,
)
}
+
+/**
+ * A type of [GridLayout] that can be paginated, to use together with [PaginatedGridLayout].
+ *
+ * [splitIntoPages] determines how to split a list of tiles based on the number of rows and columns
+ * available.
+ */
+interface PaginatableGridLayout : GridLayout {
+ fun splitIntoPages(
+ tiles: List<TileViewModel>,
+ rows: Int,
+ columns: Int,
+ ): List<List<TileViewModel>>
+
+ companion object {
+
+ /**
+ * Splits a list of [SizedTile] into rows, each with at most [columns] occupied.
+ *
+ * It will leave gaps at the end of a row if the next [SizedTile] has [SizedTile.width] that
+ * is larger than the space remaining in the row.
+ */
+ fun splitInRows(
+ tiles: List<SizedTile<TileViewModel>>,
+ columns: Int
+ ): List<List<SizedTile<TileViewModel>>> {
+ val row = TileRow<TileViewModel>(columns)
+
+ return buildList {
+ for (tile in tiles) {
+ check(tile.width <= columns)
+ if (!row.maybeAddTile(tile)) {
+ // Couldn't add tile to previous row, create a row with the current tiles
+ // and start a new one
+ add(row.tiles)
+ row.clear()
+ row.maybeAddTile(tile)
+ }
+ }
+ if (row.tiles.isNotEmpty()) {
+ add(row.tiles)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index 2f0fe22..ea97f0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -26,9 +26,10 @@
import androidx.compose.ui.res.dimensionResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.res.R
@@ -39,13 +40,14 @@
@Inject
constructor(
private val iconTilesViewModel: IconTilesViewModel,
- private val gridSizeViewModel: InfiniteGridSizeViewModel,
-) : GridLayout {
+ private val gridSizeViewModel: FixedColumnsSizeViewModel,
+) : PaginatableGridLayout {
@Composable
override fun TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
+ editModeStart: () -> Unit,
) {
DisposableEffect(tiles) {
val token = Any()
@@ -55,16 +57,8 @@
val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
- items(
- tiles.size,
- span = { index ->
- if (iconTilesViewModel.isIconTile(tiles[index].spec)) {
- GridItemSpan(1)
- } else {
- GridItemSpan(2)
- }
- }
- ) { index ->
+ items(tiles.size, span = { index -> GridItemSpan(tiles[index].spec.width()) }) { index
+ ->
Tile(
tile = tiles[index],
iconOnly = iconTilesViewModel.isIconTile(tiles[index].spec),
@@ -92,4 +86,22 @@
onRemoveTile = onRemoveTile,
)
}
+
+ override fun splitIntoPages(
+ tiles: List<TileViewModel>,
+ rows: Int,
+ columns: Int,
+ ): List<List<TileViewModel>> {
+
+ return PaginatableGridLayout.splitInRows(
+ tiles.map { SizedTile(it, it.spec.width()) },
+ columns,
+ )
+ .chunked(rows)
+ .map { it.flatten().map { it.tile } }
+ }
+
+ private fun TileSpec.width(): Int {
+ return if (iconTilesViewModel.isIconTile(this)) 1 else 2
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt
new file mode 100644
index 0000000..7de22161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.semantics.pageLeft
+import androidx.compose.ui.semantics.pageRight
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun PagerDots(
+ pagerState: PagerState,
+ activeColor: Color,
+ nonActiveColor: Color,
+ modifier: Modifier = Modifier,
+ dotSize: Dp = 6.dp,
+ spaceSize: Dp = 4.dp,
+) {
+ if (pagerState.pageCount < 2) {
+ return
+ }
+ val inPageTransition by
+ remember(pagerState) {
+ derivedStateOf {
+ pagerState.currentPageOffsetFraction.absoluteValue > 0.01 &&
+ !pagerState.isOverscrolling()
+ }
+ }
+ val coroutineScope = rememberCoroutineScope()
+ Row(
+ modifier =
+ modifier
+ .wrapContentWidth()
+ .pagerDotsSemantics(
+ pagerState,
+ coroutineScope,
+ ),
+ horizontalArrangement = spacedBy(spaceSize),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (!inPageTransition) {
+ repeat(pagerState.pageCount) { i ->
+ // We use canvas directly to only invalidate the draw phase when the page is
+ // changing.
+ Canvas(Modifier.size(dotSize)) {
+ if (pagerState.currentPage == i) {
+ drawCircle(activeColor)
+ } else {
+ drawCircle(nonActiveColor)
+ }
+ }
+ }
+ } else {
+ val doubleDotWidth = dotSize * 2 + spaceSize
+ val cornerRadius = dotSize / 2
+ val width by
+ animateDpAsState(targetValue = if (inPageTransition) doubleDotWidth else dotSize)
+
+ fun DrawScope.drawDoubleRect() {
+ drawRoundRect(
+ color = activeColor,
+ size = Size(width.toPx(), dotSize.toPx()),
+ cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx())
+ )
+ }
+
+ repeat(pagerState.pageCount) { page ->
+ Canvas(Modifier.size(dotSize)) {
+ val withPrevious = pagerState.currentPageOffsetFraction < 0
+ val ltr = layoutDirection == LayoutDirection.Ltr
+ if (
+ withPrevious && page == (pagerState.currentPage - 1) ||
+ !withPrevious && page == pagerState.currentPage
+ ) {
+ if (ltr) {
+ drawDoubleRect()
+ }
+ } else if (
+ withPrevious && page == pagerState.currentPage ||
+ !withPrevious && page == (pagerState.currentPage + 1)
+ ) {
+ if (!ltr) {
+ drawDoubleRect()
+ }
+ } else {
+ drawCircle(nonActiveColor)
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun Modifier.pagerDotsSemantics(
+ pagerState: PagerState,
+ coroutineScope: CoroutineScope,
+): Modifier {
+ return then(
+ Modifier.semantics {
+ pageLeft {
+ if (pagerState.canScrollBackward) {
+ coroutineScope.launch {
+ pagerState.animateScrollToPage(pagerState.currentPage - 1)
+ }
+ true
+ } else {
+ false
+ }
+ }
+ pageRight {
+ if (pagerState.canScrollForward) {
+ coroutineScope.launch {
+ pagerState.animateScrollToPage(pagerState.currentPage + 1)
+ }
+ true
+ } else {
+ false
+ }
+ }
+ stateDescription = "Page ${pagerState.settledPage + 1} of ${pagerState.pageCount}"
+ }
+ )
+}
+
+private fun PagerState.isOverscrolling(): Boolean {
+ val position = currentPage + currentPageOffsetFraction
+ return position < 0 || position > pageCount - 1
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
new file mode 100644
index 0000000..2ee957e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.qs.panels.ui.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing
+import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class PaginatedGridLayout
+@Inject
+constructor(
+ private val viewModel: PaginatedGridViewModel,
+ @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
+) : GridLayout by delegateGridLayout {
+ @Composable
+ override fun TileGrid(
+ tiles: List<TileViewModel>,
+ modifier: Modifier,
+ editModeStart: () -> Unit,
+ ) {
+ DisposableEffect(tiles) {
+ val token = Any()
+ tiles.forEach { it.startListening(token) }
+ onDispose { tiles.forEach { it.stopListening(token) } }
+ }
+ val columns by viewModel.columns.collectAsStateWithLifecycle()
+ val rows by viewModel.rows.collectAsStateWithLifecycle()
+
+ val pages =
+ remember(tiles, columns, rows) {
+ delegateGridLayout.splitIntoPages(tiles, rows = rows, columns = columns)
+ }
+
+ val pagerState = rememberPagerState(0) { pages.size }
+
+ Column {
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier,
+ pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp,
+ beyondViewportPageCount = 1,
+ verticalAlignment = Alignment.Top,
+ ) {
+ val page = pages[it]
+
+ delegateGridLayout.TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+ }
+ Box(
+ modifier = Modifier.height(FooterHeight).fillMaxWidth(),
+ ) {
+ PagerDots(
+ pagerState = pagerState,
+ activeColor = MaterialTheme.colorScheme.primary,
+ nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ CompositionLocalProvider(value = LocalContentColor provides Color.White) {
+ IconButton(
+ onClick = editModeStart,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(id = R.string.qs_edit)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private object Dimensions {
+ val FooterHeight = 48.dp
+ val InterPageSpacing = 16.dp
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 9233e76..7f5e474 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -53,6 +53,7 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.modifiers.background
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.PartitionedGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -63,9 +64,13 @@
@SysUISingleton
class PartitionedGridLayout @Inject constructor(private val viewModel: PartitionedGridViewModel) :
- GridLayout {
+ PaginatableGridLayout {
@Composable
- override fun TileGrid(tiles: List<TileViewModel>, modifier: Modifier) {
+ override fun TileGrid(
+ tiles: List<TileViewModel>,
+ modifier: Modifier,
+ editModeStart: () -> Unit,
+ ) {
DisposableEffect(tiles) {
val token = Any()
tiles.forEach { it.startListening(token) }
@@ -169,6 +174,20 @@
}
}
+ override fun splitIntoPages(
+ tiles: List<TileViewModel>,
+ rows: Int,
+ columns: Int,
+ ): List<List<TileViewModel>> {
+ val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) }
+
+ val sizedLargeTiles = largeTiles.map { SizedTile(it, 2) }
+ val sizedSmallTiles = smallTiles.map { SizedTile(it, 1) }
+ val largeTilesRows = PaginatableGridLayout.splitInRows(sizedLargeTiles, columns)
+ val smallTilesRows = PaginatableGridLayout.splitInRows(sizedSmallTiles, columns)
+ return (largeTilesRows + smallTilesRows).chunked(rows).map { it.flatten().map { it.tile } }
+ }
+
@Composable
private fun CurrentTiles(
tiles: List<EditTileViewModel>,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
index 7f4e0a7..4a90102 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -30,8 +30,8 @@
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.res.R
@@ -42,13 +42,14 @@
@Inject
constructor(
private val iconTilesViewModel: IconTilesViewModel,
- private val gridSizeViewModel: InfiniteGridSizeViewModel,
+ private val gridSizeViewModel: FixedColumnsSizeViewModel,
) : GridLayout {
@Composable
override fun TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
+ editModeStart: () -> Unit,
) {
DisposableEffect(tiles) {
val token = Any()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
index 2dab7c3..8c57d41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
@@ -23,9 +23,13 @@
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
@Composable
-fun TileGrid(viewModel: TileGridViewModel, modifier: Modifier = Modifier) {
+fun TileGrid(
+ viewModel: TileGridViewModel,
+ modifier: Modifier = Modifier,
+ editModeStart: () -> Unit
+) {
val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle()
val tiles by viewModel.tileViewModels.collectAsStateWithLifecycle(emptyList())
- gridLayout.TileGrid(tiles, modifier)
+ gridLayout.TileGrid(tiles, modifier, editModeStart)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
index a4ee58f..865c86b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
@@ -17,16 +17,16 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.FixedColumnsSizeInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
-interface InfiniteGridSizeViewModel {
+interface FixedColumnsSizeViewModel {
val columns: StateFlow<Int>
}
@SysUISingleton
-class InfiniteGridSizeViewModelImpl @Inject constructor(interactor: InfiniteGridSizeInteractor) :
- InfiniteGridSizeViewModel {
+class FixedColumnsSizeViewModelImpl @Inject constructor(interactor: FixedColumnsSizeInteractor) :
+ FixedColumnsSizeViewModel {
override val columns: StateFlow<Int> = interactor.columns
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
new file mode 100644
index 0000000..28bf474
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.panels.domain.interactor.PaginatedGridInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class PaginatedGridViewModel
+@Inject
+constructor(
+ iconTilesViewModel: IconTilesViewModel,
+ gridSizeViewModel: FixedColumnsSizeViewModel,
+ iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
+ paginatedGridInteractor: PaginatedGridInteractor,
+ @Application applicationScope: CoroutineScope,
+) :
+ IconTilesViewModel by iconTilesViewModel,
+ FixedColumnsSizeViewModel by gridSizeViewModel,
+ IconLabelVisibilityViewModel by iconLabelVisibilityViewModel {
+ val rows =
+ paginatedGridInteractor.rows.stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ paginatedGridInteractor.defaultRows,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
index 730cf63..2049edb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
@@ -24,9 +24,9 @@
@Inject
constructor(
iconTilesViewModel: IconTilesViewModel,
- gridSizeViewModel: InfiniteGridSizeViewModel,
+ gridSizeViewModel: FixedColumnsSizeViewModel,
iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
) :
IconTilesViewModel by iconTilesViewModel,
- InfiniteGridSizeViewModel by gridSizeViewModel,
+ FixedColumnsSizeViewModel by gridSizeViewModel,
IconLabelVisibilityViewModel by iconLabelVisibilityViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index 70f3b84..a3feb2b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -46,6 +46,7 @@
import com.android.systemui.recordissue.IssueRecordingService
import com.android.systemui.recordissue.IssueRecordingState
import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
import com.android.systemui.recordissue.TraceurMessageSender
import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingService
@@ -197,8 +198,4 @@
expandedAccessibilityClassName = Switch::class.java.name
}
}
-
- companion object {
- const val TILE_SPEC = "record_issue"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 4715230..284239a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -47,6 +47,7 @@
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.res.R;
import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
import com.android.systemui.settings.UserContextProvider;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -146,8 +147,9 @@
if (isRecording) {
state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_stop);
} else if (isStarting) {
- // round, since the timer isn't exact
- int countdown = (int) Math.floorDiv(mMillisUntilFinished + 500, 1000);
+ int countdown =
+ (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(
+ mMillisUntilFinished);
state.secondaryLabel = String.format("%d...", countdown);
} else {
state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt
new file mode 100644
index 0000000..1af328e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import com.android.systemui.Flags
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.recordissue.IssueRecordingState
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onStart
+
+class IssueRecordingDataInteractor
+@Inject
+constructor(
+ private val state: IssueRecordingState,
+ @Background private val bgCoroutineContext: CoroutineContext,
+) : QSTileDataInteractor<IssueRecordingModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<IssueRecordingModel> =
+ conflatedCallbackFlow {
+ val listener = Runnable { trySend(IssueRecordingModel(state.isRecording)) }
+ state.addListener(listener)
+ awaitClose { state.removeListener(listener) }
+ }
+ .onStart { emit(IssueRecordingModel(state.isRecording)) }
+ .distinctUntilChanged()
+ .flowOn(bgCoroutineContext)
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(android.os.Build.IS_DEBUGGABLE && Flags.recordIssueQsTile())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt
new file mode 100644
index 0000000..ff931b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.qs.tiles.impl.irecording
+
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class IssueRecordingMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Theme,
+) : QSTileDataToStateMapper<IssueRecordingModel> {
+ override fun map(config: QSTileConfig, data: IssueRecordingModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ if (data.isRecording) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ secondaryLabel = resources.getString(R.string.qs_record_issue_stop)
+ icon = { Icon.Resource(R.drawable.qs_record_issue_icon_on, null) }
+ } else {
+ icon = { Icon.Resource(R.drawable.qs_record_issue_icon_off, null) }
+ activationState = QSTileState.ActivationState.INACTIVE
+ secondaryLabel = resources.getString(R.string.qs_record_issue_start)
+ }
+ supportedActions = setOf(QSTileState.UserAction.CLICK)
+ contentDescription = "$label, $secondaryLabel"
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
index d8af3fa..260729b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.data.repository
+package com.android.systemui.qs.tiles.impl.irecording
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+@JvmInline value class IssueRecordingModel(val isRecording: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
new file mode 100644
index 0000000..4971fef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.qs.tiles.impl.irecording
+
+import android.app.AlertDialog
+import android.app.BroadcastOptions
+import android.app.PendingIntent
+import android.util.Log
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.IssueRecordingService
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
+import com.android.systemui.screenrecord.RecordingService
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+private const val TAG = "IssueRecordingActionInteractor"
+
+class IssueRecordingUserActionInteractor
+@Inject
+constructor(
+ @Main private val mainCoroutineContext: CoroutineContext,
+ private val keyguardDismissUtil: KeyguardDismissUtil,
+ private val keyguardStateController: KeyguardStateController,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val panelInteractor: PanelInteractor,
+ private val userContextProvider: UserContextProvider,
+ private val delegateFactory: RecordIssueDialogDelegate.Factory,
+) : QSTileUserActionInteractor<IssueRecordingModel> {
+
+ override suspend fun handleInput(input: QSTileInput<IssueRecordingModel>) {
+ if (input.action is QSTileUserAction.Click) {
+ if (input.data.isRecording) {
+ stopIssueRecordingService()
+ } else {
+ withContext(mainCoroutineContext) { showPrompt(input.action.expandable) }
+ }
+ } else {
+ Log.v(TAG, "the RecordIssueTile doesn't handle ${input.action} events yet.")
+ }
+ }
+
+ private fun showPrompt(expandable: Expandable?) {
+ val dialog: AlertDialog =
+ delegateFactory
+ .create {
+ startIssueRecordingService()
+ dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+ panelInteractor.collapsePanels()
+ }
+ .createDialog()
+ val dismissAction =
+ ActivityStarter.OnDismissAction {
+ // We animate from the touched view only if we are not on the keyguard, given
+ // that if we are we will dismiss it which will also collapse the shade.
+ if (expandable != null && !keyguardStateController.isShowing) {
+ expandable
+ .dialogTransitionController(
+ DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)
+ )
+ ?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show()
+ } else {
+ dialog.show()
+ }
+ false
+ }
+ keyguardDismissUtil.executeWhenUnlocked(dismissAction, false, true)
+ }
+
+ private fun startIssueRecordingService() =
+ PendingIntent.getForegroundService(
+ userContextProvider.userContext,
+ RecordingService.REQUEST_CODE,
+ IssueRecordingService.getStartIntent(userContextProvider.userContext),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+
+ private fun stopIssueRecordingService() =
+ PendingIntent.getService(
+ userContextProvider.userContext,
+ RecordingService.REQUEST_CODE,
+ IssueRecordingService.getStopIntent(userContextProvider.userContext),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
index 7446708..e74e77f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
@@ -61,7 +61,7 @@
contentDescription = null
)
icon = { loadedIcon }
- val countDown = Math.floorDiv(data.millisUntilStarted + 500, 1000)
+ val countDown = data.countdownSeconds
sideViewIcon = QSTileState.SideViewIcon.None
secondaryLabel = String.format("%d...", countDown)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
index bd748d5..e395d30 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
@@ -26,6 +26,8 @@
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeAlignment
import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,6 +39,7 @@
class QuickSettingsShadeSceneViewModel
@Inject
constructor(
+ shadeInteractor: ShadeInteractor,
val overlayShadeViewModel: OverlayShadeViewModel,
val brightnessSliderViewModel: BrightnessSliderViewModel,
val tileGridViewModel: TileGridViewModel,
@@ -46,7 +49,11 @@
val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
MutableStateFlow(
mapOf(
- Swipe.Up to SceneFamilies.Home,
+ if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) {
+ Swipe.Up
+ } else {
+ Swipe.Down
+ } to SceneFamilies.Home,
Back to SceneFamilies.Home,
)
)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index 4ea3345..b077349 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -18,7 +18,7 @@
import android.content.Context
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.tiles.RecordIssueTile
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
import com.android.systemui.res.R
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
@@ -35,11 +35,7 @@
) {
private val prefs =
- userFileManager.getSharedPreferences(
- RecordIssueTile.TILE_SPEC,
- Context.MODE_PRIVATE,
- userTracker.userId
- )
+ userFileManager.getSharedPreferences(TILE_SPEC, Context.MODE_PRIVATE, userTracker.userId)
var takeBugreport
get() = prefs.getBoolean(KEY_TAKE_BUG_REPORT, false)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
index 26af9a7..907b92c 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
@@ -16,12 +16,20 @@
package com.android.systemui.recordissue
+import com.android.systemui.Flags
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tiles.RecordIssueTile
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingDataInteractor
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingMapper
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingModel
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingUserActionInteractor
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel
import com.android.systemui.res.R
import dagger.Binds
import dagger.Module
@@ -34,19 +42,19 @@
/** Inject RecordIssueTile into tileMap in QSModule */
@Binds
@IntoMap
- @StringKey(RecordIssueTile.TILE_SPEC)
+ @StringKey(TILE_SPEC)
fun bindRecordIssueTile(recordIssueTile: RecordIssueTile): QSTileImpl<*>
companion object {
- const val RECORD_ISSUE_TILE_SPEC = "record_issue"
+ const val TILE_SPEC = "record_issue"
@Provides
@IntoMap
- @StringKey(RECORD_ISSUE_TILE_SPEC)
+ @StringKey(TILE_SPEC)
fun provideRecordIssueTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
QSTileConfig(
- tileSpec = TileSpec.create(RECORD_ISSUE_TILE_SPEC),
+ tileSpec = TileSpec.create(TILE_SPEC),
uiConfig =
QSTileUIConfig.Resource(
iconRes = R.drawable.qs_record_issue_icon_off,
@@ -54,5 +62,24 @@
),
instanceId = uiEventLogger.getNewInstanceId(),
)
+
+ /** Inject FlashlightTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(TILE_SPEC)
+ fun provideIssueRecordingTileViewModel(
+ factory: QSTileViewModelFactory.Static<IssueRecordingModel>,
+ mapper: IssueRecordingMapper,
+ stateInteractor: IssueRecordingDataInteractor,
+ userActionInteractor: IssueRecordingUserActionInteractor
+ ): QSTileViewModel =
+ if (Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 08462d7..6e89973 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -23,6 +23,7 @@
import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolverModule
import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
+import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartable
import com.android.systemui.scene.domain.startable.SceneContainerStartable
import com.android.systemui.scene.domain.startable.ScrimStartable
import com.android.systemui.scene.domain.startable.StatusBarStartable
@@ -72,6 +73,11 @@
@Binds
@IntoMap
+ @ClassKey(KeyguardStateCallbackStartable::class)
+ fun keyguardStateCallbackStartable(impl: KeyguardStateCallbackStartable): CoreStartable
+
+ @Binds
+ @IntoMap
@ClassKey(WindowRootViewVisibilityInteractor::class)
fun bindWindowRootViewVisibilityInteractor(
impl: WindowRootViewVisibilityInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 17dc9a5..7d63b4c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -24,6 +24,7 @@
import com.android.systemui.scene.domain.resolver.HomeSceneFamilyResolverModule
import com.android.systemui.scene.domain.resolver.NotifShadeSceneFamilyResolverModule
import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
+import com.android.systemui.scene.domain.startable.KeyguardStateCallbackStartable
import com.android.systemui.scene.domain.startable.SceneContainerStartable
import com.android.systemui.scene.domain.startable.ScrimStartable
import com.android.systemui.scene.domain.startable.StatusBarStartable
@@ -78,6 +79,11 @@
@Binds
@IntoMap
+ @ClassKey(KeyguardStateCallbackStartable::class)
+ fun keyguardStateCallbackStartable(impl: KeyguardStateCallbackStartable): CoreStartable
+
+ @Binds
+ @IntoMap
@ClassKey(WindowRootViewVisibilityInteractor::class)
fun bindWindowRootViewVisibilityInteractor(
impl: WindowRootViewVisibilityInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 4738dbd..25a9e9e 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -22,6 +22,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.resolver.SceneResolver
import com.android.systemui.scene.shared.logger.SceneLogger
@@ -60,6 +61,7 @@
private val logger: SceneLogger,
private val sceneFamilyResolvers: Lazy<Map<SceneKey, @JvmSuppressWildcards SceneResolver>>,
private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
+ private val keyguardEnabledInteractor: KeyguardEnabledInteractor,
) {
interface OnSceneAboutToChangeListener {
@@ -381,7 +383,8 @@
val isChangeAllowed =
to != Scenes.Gone ||
inMidTransitionFromGone ||
- deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked
+ deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked ||
+ !keyguardEnabledInteractor.isKeyguardEnabled.value
check(isChangeAllowed) {
"Cannot change to the Gone scene while the device is locked and not currently" +
" transitioning from Gone. Current transition state is ${transitionState.value}." +
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
index 9e91b66..41a3c8a 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt
@@ -22,6 +22,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import dagger.Binds
@@ -45,11 +46,13 @@
constructor(
@Application private val applicationScope: CoroutineScope,
deviceEntryInteractor: DeviceEntryInteractor,
+ keyguardEnabledInteractor: KeyguardEnabledInteractor,
) : SceneResolver {
override val targetFamily: SceneKey = SceneFamilies.Home
override val resolvedScene: StateFlow<SceneKey> =
combine(
+ keyguardEnabledInteractor.isKeyguardEnabled,
deviceEntryInteractor.canSwipeToEnter,
deviceEntryInteractor.isDeviceEntered,
deviceEntryInteractor.isUnlocked,
@@ -60,20 +63,23 @@
started = SharingStarted.Eagerly,
initialValue =
homeScene(
- deviceEntryInteractor.canSwipeToEnter.value,
- deviceEntryInteractor.isDeviceEntered.value,
- deviceEntryInteractor.isUnlocked.value,
+ isKeyguardEnabled = keyguardEnabledInteractor.isKeyguardEnabled.value,
+ canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value,
+ isDeviceEntered = deviceEntryInteractor.isDeviceEntered.value,
+ isUnlocked = deviceEntryInteractor.isUnlocked.value,
)
)
override fun includesScene(scene: SceneKey): Boolean = scene in homeScenes
private fun homeScene(
+ isKeyguardEnabled: Boolean,
canSwipeToEnter: Boolean?,
isDeviceEntered: Boolean,
isUnlocked: Boolean,
): SceneKey =
when {
+ !isKeyguardEnabled -> Scenes.Gone
canSwipeToEnter == true -> Scenes.Lockscreen
!isDeviceEntered -> Scenes.Lockscreen
!isUnlocked -> Scenes.Lockscreen
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartable.kt
new file mode 100644
index 0000000..6d1c1a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartable.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.os.DeadObjectException
+import android.os.RemoteException
+import com.android.internal.policy.IKeyguardStateCallback
+import com.android.systemui.CoreStartable
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.domain.interactor.TrustInteractor
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/** Keeps all [IKeyguardStateCallback]s hydrated with the latest state. */
+@SysUISingleton
+class KeyguardStateCallbackStartable
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val sceneInteractor: SceneInteractor,
+ private val selectedUserInteractor: SelectedUserInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val simBouncerInteractor: SimBouncerInteractor,
+ private val trustInteractor: TrustInteractor,
+) : CoreStartable {
+
+ private val callbacks = mutableListOf<IKeyguardStateCallback>()
+
+ override fun start() {
+ if (!SceneContainerFlag.isEnabled) {
+ return
+ }
+
+ hydrateKeyguardShowingAndInputRestrictionStates()
+ hydrateSimSecureState()
+ notifyWhenKeyguardShowingChanged()
+ notifyWhenTrustChanged()
+ }
+
+ fun addCallback(callback: IKeyguardStateCallback) {
+ SceneContainerFlag.assertInNewMode()
+
+ callbacks.add(callback)
+
+ applicationScope.launch(backgroundDispatcher) {
+ callback.onShowingStateChanged(
+ !deviceEntryInteractor.isDeviceEntered.value,
+ selectedUserInteractor.getSelectedUserId(),
+ )
+ callback.onTrustedChanged(trustInteractor.isTrusted.value)
+ callback.onSimSecureStateChanged(simBouncerInteractor.isAnySimSecure.value)
+ // TODO(b/348644111): add support for mNeedToReshowWhenReenabled
+ callback.onInputRestrictedStateChanged(!deviceEntryInteractor.isDeviceEntered.value)
+ }
+ }
+
+ private fun hydrateKeyguardShowingAndInputRestrictionStates() {
+ applicationScope.launch {
+ combine(
+ selectedUserInteractor.selectedUser,
+ deviceEntryInteractor.isDeviceEntered,
+ ::Pair
+ )
+ .collectLatest { (selectedUserId, isDeviceEntered) ->
+ val iterator = callbacks.iterator()
+ withContext(backgroundDispatcher) {
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ try {
+ callback.onShowingStateChanged(!isDeviceEntered, selectedUserId)
+ // TODO(b/348644111): add support for mNeedToReshowWhenReenabled
+ callback.onInputRestrictedStateChanged(!isDeviceEntered)
+ } catch (e: RemoteException) {
+ if (e is DeadObjectException) {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun hydrateSimSecureState() {
+ applicationScope.launch {
+ simBouncerInteractor.isAnySimSecure.collectLatest { isSimSecured ->
+ val iterator = callbacks.iterator()
+ withContext(backgroundDispatcher) {
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ try {
+ callback.onSimSecureStateChanged(isSimSecured)
+ } catch (e: RemoteException) {
+ if (e is DeadObjectException) {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun notifyWhenKeyguardShowingChanged() {
+ applicationScope.launch {
+ // This is equivalent to isDeviceEntered but it waits for the full transition animation
+ // to finish before emitting a new value and not just for the current scene to be
+ // switched.
+ sceneInteractor.transitionState
+ .filter { it.isIdle(Scenes.Gone) || it.isIdle(Scenes.Lockscreen) }
+ .map { it.isIdle(Scenes.Lockscreen) }
+ .distinctUntilChanged()
+ .collectLatest { trustInteractor.reportKeyguardShowingChanged() }
+ }
+ }
+
+ private fun notifyWhenTrustChanged() {
+ applicationScope.launch {
+ trustInteractor.isTrusted.collectLatest { isTrusted ->
+ val iterator = callbacks.iterator()
+ withContext(backgroundDispatcher) {
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ try {
+ callback.onTrustedChanged(isTrusted)
+ } catch (e: RemoteException) {
+ if (e is DeadObjectException) {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 1e689bd..fbc0748 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -38,6 +38,7 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
+import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor
import com.android.systemui.model.SceneContainerPlugin
@@ -82,6 +83,7 @@
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -123,6 +125,7 @@
private val sceneBackInteractor: SceneBackInteractor,
private val shadeSessionStorage: SessionStorage,
private val windowMgrLockscreenVisInteractor: WindowManagerLockscreenVisibilityInteractor,
+ private val keyguardEnabledInteractor: KeyguardEnabledInteractor,
) : CoreStartable {
private val centralSurfaces: CentralSurfaces?
get() = centralSurfacesOptLazy.get().getOrNull()
@@ -140,6 +143,7 @@
hydrateWindowController()
hydrateBackStack()
resetShadeSessions()
+ handleKeyguardEnabledness()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -650,6 +654,51 @@
}
}
+ private fun handleKeyguardEnabledness() {
+ // Automatically switches scenes when keyguard is enabled or disabled, as needed.
+ applicationScope.launch {
+ keyguardEnabledInteractor.isKeyguardEnabled
+ .sample(
+ combine(
+ deviceEntryInteractor.isInLockdown,
+ deviceEntryInteractor.isDeviceEntered,
+ ::Pair,
+ )
+ ) { isKeyguardEnabled, (isInLockdown, isDeviceEntered) ->
+ when {
+ !isKeyguardEnabled && !isInLockdown && !isDeviceEntered -> {
+ keyguardEnabledInteractor.setShowKeyguardWhenReenabled(true)
+ Scenes.Gone to "Keyguard became disabled"
+ }
+ isKeyguardEnabled &&
+ keyguardEnabledInteractor.isShowKeyguardWhenReenabled() -> {
+ keyguardEnabledInteractor.setShowKeyguardWhenReenabled(false)
+ Scenes.Lockscreen to "Keyguard became enabled"
+ }
+ else -> null
+ }
+ }
+ .filterNotNull()
+ .collect { (targetScene, loggingReason) ->
+ switchToScene(targetScene, loggingReason)
+ }
+ }
+
+ // Clears the showKeyguardWhenReenabled if the auth method changes to an insecure one.
+ applicationScope.launch {
+ authenticationInteractor
+ .get()
+ .authenticationMethod
+ .map { it.isSecure }
+ .distinctUntilChanged()
+ .collect { isAuthenticationMethodSecure ->
+ if (!isAuthenticationMethodSecure) {
+ keyguardEnabledInteractor.setShowKeyguardWhenReenabled(false)
+ }
+ }
+ }
+ }
+
private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) {
sceneInteractor.changeScene(
toScene = targetSceneKey,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index cf33c4a..6c63c97 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -39,13 +39,14 @@
inline val isEnabled
get() =
sceneContainer() && // mainAconfigFlag
- ComposeLockscreen.isEnabled &&
+ ComposeLockscreen.isEnabled &&
KeyguardBottomAreaRefactor.isEnabled &&
KeyguardWmStateRefactor.isEnabled &&
MigrateClocksToBlueprint.isEnabled &&
NotificationsHeadsUpRefactor.isEnabled &&
PredictiveBackSysUiFlag.isEnabled &&
DeviceEntryUdfpsRefactor.isEnabled
+
// NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
/** The main aconfig flag. */
@@ -91,6 +92,14 @@
@JvmStatic
inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, DESCRIPTION)
+ /**
+ * Called to ensure the new code is only run when the flag is enabled. This will throw an
+ * exception if the flag is disabled to ensure that the refactor author catches issues in
+ * testing.
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, DESCRIPTION)
+
/** Returns a developer-readable string that describes the current requirement list. */
@JvmStatic
fun requirementDescription(): String {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index ac91337..9f48ee9 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -64,14 +64,25 @@
// TODO(b/338577208): Remove this once we add Dual Shade invocation zones.
shadeMode is ShadeMode.Dual
) {
- put(
- Swipe(
- pointerCount = 2,
- fromSource = Edge.Top,
- direction = SwipeDirection.Down,
- ),
- UserActionResult(SceneFamilies.QuickSettings)
- )
+ if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) {
+ put(
+ Swipe(
+ pointerCount = 2,
+ fromSource = Edge.Bottom,
+ direction = SwipeDirection.Up,
+ ),
+ UserActionResult(SceneFamilies.QuickSettings, OpenBottomShade)
+ )
+ } else {
+ put(
+ Swipe(
+ pointerCount = 2,
+ fromSource = Edge.Top,
+ direction = SwipeDirection.Down,
+ ),
+ UserActionResult(SceneFamilies.QuickSettings)
+ )
+ }
}
if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
index b225444..ada5d8c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
@@ -22,7 +22,17 @@
data object Recording : ScreenRecordModel
/** A screen recording will begin in [millisUntilStarted] ms. */
- data class Starting(val millisUntilStarted: Long) : ScreenRecordModel
+ data class Starting(val millisUntilStarted: Long) : ScreenRecordModel {
+ val countdownSeconds = millisUntilStarted.toCountdownSeconds()
+
+ companion object {
+ /**
+ * Returns the number of seconds until screen recording will start, used to show a 3-2-1
+ * countdown.
+ */
+ fun Long.toCountdownSeconds() = Math.floorDiv(this + 500, 1000)
+ }
+ }
/** There's nothing related to screen recording happening. */
data object DoingNothing : ScreenRecordModel
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
new file mode 100644
index 0000000..6730d2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.screenshot
+
+import android.net.Uri
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManager
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * A ScreenshotHandler that just saves the screenshot and calls back as appropriate, with no UI.
+ *
+ * Basically, ScreenshotController with all the UI bits ripped out.
+ */
+class HeadlessScreenshotHandler
+@Inject
+constructor(
+ private val imageExporter: ImageExporter,
+ @Main private val mainExecutor: Executor,
+ private val imageCapture: ImageCapture,
+ private val userManager: UserManager,
+ private val uiEventLogger: UiEventLogger,
+ private val notificationsControllerFactory: ScreenshotNotificationsController.Factory,
+) : ScreenshotHandler {
+
+ override fun handleScreenshot(
+ screenshot: ScreenshotData,
+ finisher: Consumer<Uri?>,
+ requestCallback: TakeScreenshotService.RequestCallback
+ ) {
+ if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
+ screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null)
+ }
+
+ if (screenshot.bitmap == null) {
+ Log.e(TAG, "handleScreenshot: Screenshot bitmap was null")
+ notificationsControllerFactory
+ .create(screenshot.displayId)
+ .notifyScreenshotError(R.string.screenshot_failed_to_capture_text)
+ requestCallback.reportError()
+ return
+ }
+
+ val future: ListenableFuture<ImageExporter.Result> =
+ imageExporter.export(
+ Executors.newSingleThreadExecutor(),
+ UUID.randomUUID(),
+ screenshot.bitmap,
+ screenshot.getUserOrDefault(),
+ screenshot.displayId
+ )
+ future.addListener(
+ {
+ try {
+ val result = future.get()
+ Log.d(TAG, "Saved screenshot: $result")
+ logScreenshotResultStatus(result.uri, screenshot)
+ finisher.accept(result.uri)
+ requestCallback.onFinish()
+ } catch (e: Exception) {
+ Log.d(TAG, "Failed to store screenshot", e)
+ finisher.accept(null)
+ requestCallback.reportError()
+ }
+ },
+ mainExecutor
+ )
+ }
+
+ private fun logScreenshotResultStatus(uri: Uri?, screenshot: ScreenshotData) {
+ if (uri == null) {
+ uiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, screenshot.packageNameString)
+ notificationsControllerFactory
+ .create(screenshot.displayId)
+ .notifyScreenshotError(R.string.screenshot_failed_to_save_text)
+ } else {
+ uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString)
+ if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) {
+ uiEventLogger.log(
+ ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE,
+ 0,
+ screenshot.packageNameString
+ )
+ }
+ }
+ }
+
+ companion object {
+ const val TAG = "HeadlessScreenshotHandler"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index e8dfac8..c87b1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -101,7 +101,7 @@
/**
* Controls the state and flow for screenshots.
*/
-public class ScreenshotController {
+public class ScreenshotController implements ScreenshotHandler {
private static final String TAG = logTag(ScreenshotController.class);
/**
@@ -351,7 +351,8 @@
mShowUIOnExternalDisplay = showUIOnExternalDisplay;
}
- void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
+ @Override
+ public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
RequestCallback requestCallback) {
Assert.isMainThread();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 3c3797b..2699657 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.screenshot
import android.net.Uri
@@ -7,12 +23,12 @@
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import com.android.internal.logging.UiEventLogger
import com.android.internal.util.ScreenshotRequest
-import com.android.systemui.Flags.screenshotShelfUi2
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.res.R
import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
import java.util.function.Consumer
import javax.inject.Inject
@@ -26,9 +42,13 @@
onSaved: (Uri?) -> Unit,
requestCallback: RequestCallback
)
+
fun onCloseSystemDialogsReceived()
+
fun removeWindows()
+
fun onDestroy()
+
fun executeScreenshotsAsync(
screenshotRequest: ScreenshotRequest,
onSaved: Consumer<Uri?>,
@@ -36,6 +56,14 @@
)
}
+interface ScreenshotHandler {
+ fun handleScreenshot(
+ screenshot: ScreenshotData,
+ finisher: Consumer<Uri?>,
+ requestCallback: RequestCallback
+ )
+}
+
/**
* Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
* result.
@@ -52,10 +80,10 @@
private val screenshotRequestProcessor: ScreenshotRequestProcessor,
private val uiEventLogger: UiEventLogger,
private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory,
+ private val headlessScreenshotHandler: HeadlessScreenshotHandler,
) : TakeScreenshotExecutor {
-
private val displays = displayRepository.displays
- private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()
+ private var screenshotController: ScreenshotController? = null
private val notificationControllers = mutableMapOf<Int, ScreenshotNotificationsController>()
/**
@@ -73,9 +101,15 @@
val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
displays.forEach { display ->
val displayId = display.displayId
+ var screenshotHandler: ScreenshotHandler =
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ getScreenshotController(display)
+ } else {
+ headlessScreenshotHandler
+ }
Log.d(TAG, "Executing screenshot for display $displayId")
dispatchToController(
- display = display,
+ screenshotHandler,
rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
onSaved =
if (displayId == Display.DEFAULT_DISPLAY) {
@@ -88,7 +122,7 @@
/** All logging should be triggered only by this method. */
private suspend fun dispatchToController(
- display: Display,
+ screenshotHandler: ScreenshotHandler,
rawScreenshotData: ScreenshotData,
onSaved: (Uri?) -> Unit,
callback: RequestCallback
@@ -102,13 +136,12 @@
logScreenshotRequested(rawScreenshotData)
onFailedScreenshotRequest(rawScreenshotData, callback)
}
- .getOrNull()
- ?: return
+ .getOrNull() ?: return
logScreenshotRequested(screenshotData)
Log.d(TAG, "Screenshot request: $screenshotData")
try {
- getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback)
+ screenshotHandler.handleScreenshot(screenshotData, onSaved, callback)
} catch (e: IllegalStateException) {
Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e)
onFailedScreenshotRequest(screenshotData, callback)
@@ -140,44 +173,32 @@
private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> {
val allDisplays = displays.first()
- return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE || screenshotShelfUi2()) {
- // If this is a provided image or using the shelf UI, just screenshot th default display
+ return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+ // If this is a provided image just screenshot th default display
allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY }
} else {
allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES }
}
}
- /** Propagates the close system dialog signal to all controllers. */
+ /** Propagates the close system dialog signal to the ScreenshotController. */
override fun onCloseSystemDialogsReceived() {
- screenshotControllers.forEach { (_, screenshotController) ->
- if (!screenshotController.isPendingSharedTransition) {
- screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
- }
+ if (screenshotController?.isPendingSharedTransition == false) {
+ screenshotController?.requestDismissal(SCREENSHOT_DISMISSED_OTHER)
}
}
/** Removes all screenshot related windows. */
override fun removeWindows() {
- screenshotControllers.forEach { (_, screenshotController) ->
- screenshotController.removeWindow()
- }
+ screenshotController?.removeWindow()
}
/**
* Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
*/
override fun onDestroy() {
- screenshotControllers.forEach { (_, screenshotController) ->
- screenshotController.onDestroy()
- }
- screenshotControllers.clear()
- }
-
- private fun getScreenshotController(display: Display): ScreenshotController {
- return screenshotControllers.computeIfAbsent(display.displayId) {
- screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false)
- }
+ screenshotController?.onDestroy()
+ screenshotController = null
}
private fun getNotificationController(id: Int): ScreenshotNotificationsController {
@@ -197,6 +218,12 @@
}
}
+ private fun getScreenshotController(display: Display): ScreenshotController {
+ val controller = screenshotController ?: screenshotControllerFactory.create(display, false)
+ screenshotController = controller
+ return controller
+ }
+
/**
* Returns a [RequestCallback] that wraps [originalCallback].
*
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index d382b7a..a62edcb 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -25,6 +25,7 @@
import android.app.Activity;
import android.content.res.Configuration;
+import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.Gravity;
@@ -32,7 +33,9 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
+import android.view.WindowInsets;
import android.view.WindowManager;
+import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
@@ -152,18 +155,27 @@
Configuration configuration = getResources().getConfiguration();
int orientation = configuration.orientation;
- int screenWidth = getWindowManager().getDefaultDisplay().getWidth();
+ int windowWidth = getWindowAvailableWidth();
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
boolean shouldBeFullWidth = getIntent()
.getBooleanExtra(EXTRA_BRIGHTNESS_DIALOG_IS_FULL_WIDTH, false);
- lp.width = (shouldBeFullWidth ? screenWidth : screenWidth / 2) - horizontalMargin * 2;
+ lp.width = (shouldBeFullWidth ? windowWidth : windowWidth / 2) - horizontalMargin * 2;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
- lp.width = screenWidth - horizontalMargin * 2;
+ lp.width = windowWidth - horizontalMargin * 2;
}
frame.setLayoutParams(lp);
+ }
+ private int getWindowAvailableWidth() {
+ final WindowMetrics metrics = getWindowManager().getCurrentWindowMetrics();
+ // Gets all excluding insets
+ final WindowInsets windowInsets = metrics.getWindowInsets();
+ Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
+ | WindowInsets.Type.displayCutout());
+ int insetsWidth = insets.right + insets.left;
+ return metrics.getBounds().width() - insetsWidth;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index ee8161c..ce321dc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -182,7 +182,11 @@
}
override fun expandToQs() {
- sceneInteractor.changeScene(SceneFamilies.QuickSettings, "ShadeController.animateExpandQs")
+ sceneInteractor.changeScene(
+ SceneFamilies.QuickSettings,
+ "ShadeController.animateExpandQs",
+ OpenBottomShade.takeIf { shadeInteractor.shadeAlignment == Alignment.BottomEnd }
+ )
}
override fun setVisibilityListener(listener: ShadeVisibilityListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
index b946129..6551854 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
@@ -51,7 +51,7 @@
initialValue = Scenes.Lockscreen,
)
- /** Dictates whether the panel is aligned to the top or the bottom. */
+ /** Dictates the alignment of the overlay shade panel on the screen. */
val panelAlignment = shadeInteractor.shadeAlignment
/** Notifies that the user has clicked the semi-transparent background scrim. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 1a7871a..2f3fc729 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1670,11 +1670,6 @@
private final StatusBarStateController.StateListener mStatusBarStateListener =
new StatusBarStateController.StateListener() {
@Override
- public void onStateChanged(int newState) {
- setVisible(newState == StatusBarState.KEYGUARD);
- }
-
- @Override
public void onDozingChanged(boolean dozing) {
if (mDozing == dozing) {
return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 855798c..28e3a83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -94,7 +94,6 @@
private float mCornerAnimationDistance;
private float mActualWidth = -1;
private int mMaxIconsOnLockscreen;
- private int mNotificationScrimPadding;
private boolean mCanModifyColorOfNotifications;
private boolean mCanInteract;
private NotificationStackScrollLayout mHostLayout;
@@ -138,7 +137,6 @@
mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen);
- mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
ViewGroup.LayoutParams layoutParams = getLayoutParams();
final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -265,7 +263,7 @@
}
final float stackBottom = SceneContainerFlag.isEnabled()
- ? getStackBottom(ambientState)
+ ? ambientState.getStackTop() + ambientState.getStackHeight()
: ambientState.getStackY() + ambientState.getStackHeight();
if (viewState.hidden) {
@@ -278,19 +276,6 @@
}
}
- /**
- * bottom-most position, where we can draw the stack
- */
- private float getStackBottom(AmbientState ambientState) {
- if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
- float stackBottom = ambientState.getStackCutoff() - mNotificationScrimPadding;
- if (ambientState.isExpansionChanging()) {
- stackBottom = MathUtils.lerp(stackBottom * StackScrollAlgorithm.START_FRACTION,
- stackBottom, ambientState.getExpansionFraction());
- }
- return stackBottom;
- }
-
private int getSpeedBumpIndex() {
NotificationIconContainerRefactor.assertInLegacyMode();
return mHostLayout.getSpeedBumpIndex();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index ba3fde6..79f1874 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.call.ui.viewmodel
+import android.view.View
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.common.shared.model.Icon
@@ -60,7 +61,7 @@
val startTimeInElapsedRealtime =
state.startTimeMs - systemClock.currentTimeMillis() +
systemClock.elapsedRealtime()
- OngoingActivityChipModel.Shown(
+ OngoingActivityChipModel.Shown.Timer(
icon =
Icon.Resource(
com.android.internal.R.drawable.ic_phone,
@@ -68,26 +69,30 @@
),
colors = ColorsModel.Themed,
startTimeMs = startTimeInElapsedRealtime,
- ) {
- if (state.intent != null) {
- val backgroundView =
- it.requireViewById<ChipBackgroundContainer>(
- R.id.ongoing_activity_chip_background
- )
- // TODO(b/332662551): Log the click event.
- // This mimics OngoingCallController#updateChipClickListener.
- activityStarter.postStartActivityDismissingKeyguard(
- state.intent,
- ActivityTransitionAnimator.Controller.fromView(
- backgroundView,
- InteractionJankMonitor
- .CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
- )
- )
- }
- }
+ getOnClickListener(state),
+ )
}
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+ private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? {
+ if (state.intent == null) {
+ return null
+ }
+
+ return View.OnClickListener { view ->
+ val backgroundView =
+ view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background)
+ // TODO(b/332662551): Log the click event.
+ // This mimics OngoingCallController#updateChipClickListener.
+ activityStarter.postStartActivityDismissingKeyguard(
+ state.intent,
+ ActivityTransitionAnimator.Controller.fromView(
+ backgroundView,
+ InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
+ )
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 53b1e75..42e921e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -70,7 +70,8 @@
}
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ // See b/347726238.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
/** Stops the currently active projection. */
private fun stopProjecting() {
@@ -80,7 +81,7 @@
private fun createCastToOtherDeviceChip(
state: ProjectionChipModel.Projecting,
): OngoingActivityChipModel.Shown {
- return OngoingActivityChipModel.Shown(
+ return OngoingActivityChipModel.Shown.Timer(
icon =
Icon.Resource(
CAST_TO_OTHER_DEVICE_ICON,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 1e9f0a1..43b1d16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -50,7 +50,6 @@
) { screenRecordState, mediaProjectionState ->
when (screenRecordState) {
is ScreenRecordModel.DoingNothing -> ScreenRecordChipModel.DoingNothing
- // TODO(b/332662551): Implement the 3-2-1 countdown chip.
is ScreenRecordModel.Starting ->
ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
is ScreenRecordModel.Recording -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 9d54c75..af6d7f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -23,6 +23,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
@@ -55,10 +56,14 @@
.map { state ->
when (state) {
is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden
- // TODO(b/332662551): Implement the 3-2-1 countdown chip.
- is ScreenRecordChipModel.Starting -> OngoingActivityChipModel.Hidden
+ is ScreenRecordChipModel.Starting -> {
+ OngoingActivityChipModel.Shown.Countdown(
+ colors = ColorsModel.Red,
+ secondsUntilStarted = state.millisUntilStarted.toCountdownSeconds(),
+ )
+ }
is ScreenRecordChipModel.Recording -> {
- OngoingActivityChipModel.Shown(
+ OngoingActivityChipModel.Shown.Timer(
// TODO(b/332662551): Also provide a content description.
icon = Icon.Resource(ICON, contentDescription = null),
colors = ColorsModel.Red,
@@ -71,7 +76,8 @@
}
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ // See b/347726238.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
private fun createDelegate(
recordedTask: ActivityManager.RunningTaskInfo?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index 0c24a70..c3b1456 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -66,7 +66,8 @@
}
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ // See b/347726238.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
/** Stops the currently active projection. */
private fun stopProjecting() {
@@ -76,7 +77,7 @@
private fun createShareToAppChip(
state: ProjectionChipModel.Projecting,
): OngoingActivityChipModel.Shown {
- return OngoingActivityChipModel.Shown(
+ return OngoingActivityChipModel.Shown.Timer(
// TODO(b/332662551): Use the right content description.
icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null),
colors = ColorsModel.Red,
@@ -97,7 +98,6 @@
)
companion object {
- // TODO(b/332662551): Use the right icon.
- @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_screenshot_share
+ @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 4ea674a..57f609b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -25,22 +25,42 @@
data object Hidden : OngoingActivityChipModel()
/** This chip should be shown with the given information. */
- data class Shown(
- /** The icon to show on the chip. */
- val icon: Icon,
+ abstract class Shown(
+ /** The icon to show on the chip. If null, no icon will be shown. */
+ open val icon: Icon?,
/** What colors to use for the chip. */
- val colors: ColorsModel,
+ open val colors: ColorsModel,
/**
- * The time this event started, used to show the timer.
- *
- * This time should be relative to
- * [com.android.systemui.util.time.SystemClock.elapsedRealtime], *not*
- * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because the
- * [ChipChronometer] is based off of elapsed realtime. See
- * [android.widget.Chronometer.setBase].
+ * Listener method to invoke when this chip is clicked. If null, the chip won't be
+ * clickable.
*/
- val startTimeMs: Long,
- /** Listener method to invoke when this chip is clicked. */
- val onClickListener: View.OnClickListener,
- ) : OngoingActivityChipModel()
+ open val onClickListener: View.OnClickListener?,
+ ) : OngoingActivityChipModel() {
+ /** The chip shows a timer, counting up from [startTimeMs]. */
+ data class Timer(
+ override val icon: Icon,
+ override val colors: ColorsModel,
+ /**
+ * The time this event started, used to show the timer.
+ *
+ * This time should be relative to
+ * [com.android.systemui.util.time.SystemClock.elapsedRealtime], *not*
+ * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because the
+ * [ChipChronometer] is based off of elapsed realtime. See
+ * [android.widget.Chronometer.setBase].
+ */
+ val startTimeMs: Long,
+ override val onClickListener: View.OnClickListener?,
+ ) : Shown(icon, colors, onClickListener)
+
+ /**
+ * This chip shows a countdown using [secondsUntilStarted]. Used to inform users that an
+ * event is about to start. Typically, a [Countdown] chip will turn into a [Timer] chip.
+ */
+ data class Countdown(
+ override val colors: ColorsModel,
+ /** The number of seconds until an event is started. */
+ val secondsUntilStarted: Long,
+ ) : Shown(icon = null, colors, onClickListener = null)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 1b79ce4..9c8086f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -72,5 +72,8 @@
else -> call
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+ // Some of the chips could have timers in them and we don't want the start time
+ // for those timers to get reset for any reason. So, as soon as any subscriber has
+ // requested the chip information, we need to maintain it forever. See b/347726238.
+ .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 4c66f66..0bb18d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -58,7 +58,7 @@
constructor(
@Application applicationScope: CoroutineScope,
dumpManager: DumpManager,
- private val mHeadsUpManager: HeadsUpManager,
+ private val headsUpManager: HeadsUpManager,
private val statusBarStateController: StatusBarStateController,
private val bypassController: KeyguardBypassController,
private val dozeParameters: DozeParameters,
@@ -71,8 +71,8 @@
StatusBarStateController.StateListener,
ShadeExpansionListener,
Dumpable {
- private lateinit var mStackScrollerController: NotificationStackScrollLayoutController
- private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
+ private lateinit var stackScrollerController: NotificationStackScrollLayoutController
+ private var visibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
private var inputLinearDozeAmount: Float = 0.0f
private var inputEasedDozeAmount: Float = 0.0f
@@ -85,13 +85,13 @@
private var outputEasedDozeAmount: Float = 0.0f
@VisibleForTesting val dozeAmountInterpolator: Interpolator = Interpolators.FAST_OUT_SLOW_IN
- private var mNotificationVisibleAmount = 0.0f
- private var mNotificationsVisible = false
- private var mNotificationsVisibleForExpansion = false
- private var mVisibilityAnimator: ObjectAnimator? = null
- private var mVisibilityAmount = 0.0f
- private var mLinearVisibilityAmount = 0.0f
- private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
+ private var notificationVisibleAmount = 0.0f
+ private var notificationsVisible = false
+ private var notificationsVisibleForExpansion = false
+ private var visibilityAnimator: ObjectAnimator? = null
+ private var visibilityAmount = 0.0f
+ private var linearVisibilityAmount = 0.0f
+ private val entrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
private var pulseExpanding: Boolean = false
private val wakeUpListeners = arrayListOf<WakeUpListener>()
private var state: Int = StatusBarState.KEYGUARD
@@ -104,14 +104,14 @@
willWakeUp = false
if (value) {
if (
- mNotificationsVisible &&
- !mNotificationsVisibleForExpansion &&
+ notificationsVisible &&
+ !notificationsVisibleForExpansion &&
!bypassController.bypassEnabled
) {
// We're waking up while pulsing, let's make sure the animation looks nice
- mStackScrollerController.wakeUpFromPulse()
+ stackScrollerController.wakeUpFromPulse()
}
- if (bypassController.bypassEnabled && !mNotificationsVisible) {
+ if (bypassController.bypassEnabled && !notificationsVisible) {
// Let's make sure our huns become visible once we are waking up in case
// they were blocked by the proximity sensor
updateNotificationVisibility(
@@ -186,13 +186,13 @@
init {
dumpManager.registerDumpable(this)
- mHeadsUpManager.addListener(this)
+ headsUpManager.addListener(this)
statusBarStateController.addCallback(this)
bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
addListener(
object : WakeUpListener {
override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
- if (isFullyHidden && mNotificationsVisibleForExpansion) {
+ if (isFullyHidden && notificationsVisibleForExpansion) {
// When the notification becomes fully invisible, let's make sure our
// expansion
// flag also changes. This can happen if the bouncer shows when dragging
@@ -217,7 +217,7 @@
}
fun setStackScroller(stackScrollerController: NotificationStackScrollLayoutController) {
- mStackScrollerController = stackScrollerController
+ this.stackScrollerController = stackScrollerController
pulseExpanding = stackScrollerController.isPulseExpanding
stackScrollerController.setOnPulseHeightChangedListener {
val nowExpanding = isPulseExpanding()
@@ -237,7 +237,7 @@
}
}
- fun isPulseExpanding(): Boolean = mStackScrollerController.isPulseExpanding
+ fun isPulseExpanding(): Boolean = stackScrollerController.isPulseExpanding
/**
* @param visible should notifications be visible
@@ -249,13 +249,13 @@
animate: Boolean,
increaseSpeed: Boolean
) {
- mNotificationsVisibleForExpansion = visible
+ notificationsVisibleForExpansion = visible
updateNotificationVisibility(animate, increaseSpeed)
- if (!visible && mNotificationsVisible) {
+ if (!visible && notificationsVisible) {
// If we stopped expanding and we're still visible because we had a pulse that hasn't
// times out, let's release them all to make sure were not stuck in a state where
// notifications are visible
- mHeadsUpManager.releaseAllImmediately()
+ headsUpManager.releaseAllImmediately()
}
}
@@ -269,12 +269,12 @@
private fun updateNotificationVisibility(animate: Boolean, increaseSpeed: Boolean) {
// TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore
- var visible = mNotificationsVisibleForExpansion || mHeadsUpManager.hasNotifications()
+ var visible = notificationsVisibleForExpansion || headsUpManager.hasNotifications()
visible = visible && canShowPulsingHuns
if (
!visible &&
- mNotificationsVisible &&
+ notificationsVisible &&
(wakingUp || willWakeUp) &&
outputLinearDozeAmount != 0.0f
) {
@@ -290,11 +290,11 @@
animate: Boolean,
increaseSpeed: Boolean
) {
- if (mNotificationsVisible == visible) {
+ if (notificationsVisible == visible) {
return
}
- mNotificationsVisible = visible
- mVisibilityAnimator?.cancel()
+ notificationsVisible = visible
+ visibilityAnimator?.cancel()
if (animate) {
notifyAnimationStart(visible)
startVisibilityAnimation(increaseSpeed)
@@ -371,7 +371,7 @@
state = statusBarStateController.state,
changed = changed
)
- mStackScrollerController.setDozeAmount(outputEasedDozeAmount)
+ stackScrollerController.setDozeAmount(outputEasedDozeAmount)
updateHideAmount()
if (changed && outputLinearDozeAmount == 0.0f) {
setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
@@ -475,7 +475,7 @@
this.collapsedEnoughToHide = collapsedEnough
if (couldShowPulsingHuns && !canShowPulsingHuns) {
updateNotificationVisibility(animate = true, increaseSpeed = true)
- mHeadsUpManager.releaseAllImmediately()
+ headsUpManager.releaseAllImmediately()
}
}
}
@@ -562,12 +562,12 @@
}
private fun startVisibilityAnimation(increaseSpeed: Boolean) {
- if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
- mVisibilityInterpolator =
- if (mNotificationsVisible) Interpolators.TOUCH_RESPONSE
+ if (notificationVisibleAmount == 0f || notificationVisibleAmount == 1f) {
+ visibilityInterpolator =
+ if (notificationsVisible) Interpolators.TOUCH_RESPONSE
else Interpolators.FAST_OUT_SLOW_IN_REVERSE
}
- val target = if (mNotificationsVisible) 1.0f else 0.0f
+ val target = if (notificationsVisible) 1.0f else 0.0f
val visibilityAnimator = ObjectAnimator.ofFloat(this, notificationVisibility, target)
visibilityAnimator.interpolator = InterpolatorsAndroidX.LINEAR
var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
@@ -576,34 +576,34 @@
}
visibilityAnimator.duration = duration
visibilityAnimator.start()
- mVisibilityAnimator = visibilityAnimator
+ this.visibilityAnimator = visibilityAnimator
}
private fun setVisibilityAmount(visibilityAmount: Float) {
logger.logSetVisibilityAmount(visibilityAmount)
- mLinearVisibilityAmount = visibilityAmount
- mVisibilityAmount = mVisibilityInterpolator.getInterpolation(visibilityAmount)
+ linearVisibilityAmount = visibilityAmount
+ this.visibilityAmount = visibilityInterpolator.getInterpolation(visibilityAmount)
handleAnimationFinished()
updateHideAmount()
}
private fun handleAnimationFinished() {
- if (outputLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) {
- mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
- mEntrySetToClearWhenFinished.clear()
+ if (outputLinearDozeAmount == 0.0f || linearVisibilityAmount == 0.0f) {
+ entrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
+ entrySetToClearWhenFinished.clear()
}
}
private fun updateHideAmount() {
- val linearAmount = min(1.0f - mLinearVisibilityAmount, outputLinearDozeAmount)
- val amount = min(1.0f - mVisibilityAmount, outputEasedDozeAmount)
+ val linearAmount = min(1.0f - linearVisibilityAmount, outputLinearDozeAmount)
+ val amount = min(1.0f - visibilityAmount, outputEasedDozeAmount)
logger.logSetHideAmount(linearAmount)
- mStackScrollerController.setHideAmount(linearAmount, amount)
+ stackScrollerController.setHideAmount(linearAmount, amount)
notificationsFullyHidden = linearAmount == 1.0f
}
private fun notifyAnimationStart(awake: Boolean) {
- mStackScrollerController.notifyHideAnimationStart(!awake)
+ stackScrollerController.notifyHideAnimationStart(!awake)
}
override fun onDozingChanged(isDozing: Boolean) {
@@ -615,7 +615,7 @@
override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
var animate = shouldAnimateVisibility()
if (!isHeadsUp) {
- if (outputLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) {
+ if (outputLinearDozeAmount != 0.0f && linearVisibilityAmount != 0.0f) {
if (entry.isRowDismissed) {
// if we animate, we see the shelf briefly visible. Instead we fully animate
// the notification and its background out
@@ -623,11 +623,11 @@
} else if (!wakingUp && !willWakeUp) {
// TODO: look that this is done properly and not by anyone else
entry.setHeadsUpAnimatingAway(true)
- mEntrySetToClearWhenFinished.add(entry)
+ entrySetToClearWhenFinished.add(entry)
}
}
- } else if (mEntrySetToClearWhenFinished.contains(entry)) {
- mEntrySetToClearWhenFinished.remove(entry)
+ } else if (entrySetToClearWhenFinished.contains(entry)) {
+ entrySetToClearWhenFinished.remove(entry)
entry.setHeadsUpAnimatingAway(false)
}
updateNotificationVisibility(animate, increaseSpeed = false)
@@ -644,11 +644,11 @@
pw.println("hardDozeAmountOverrideSource: $hardDozeAmountOverrideSource")
pw.println("outputLinearDozeAmount: $outputLinearDozeAmount")
pw.println("outputEasedDozeAmount: $outputEasedDozeAmount")
- pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
- pw.println("mNotificationsVisible: $mNotificationsVisible")
- pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
- pw.println("mVisibilityAmount: $mVisibilityAmount")
- pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount")
+ pw.println("notificationVisibleAmount: $notificationVisibleAmount")
+ pw.println("notificationsVisible: $notificationsVisible")
+ pw.println("notificationsVisibleForExpansion: $notificationsVisibleForExpansion")
+ pw.println("visibilityAmount: $visibilityAmount")
+ pw.println("linearVisibilityAmount: $linearVisibilityAmount")
pw.println("pulseExpanding: $pulseExpanding")
pw.println("state: ${StatusBarState.toString(state)}")
pw.println("fullyAwake: $fullyAwake")
@@ -698,7 +698,7 @@
}
override fun get(coordinator: NotificationWakeUpCoordinator): Float {
- return coordinator.mLinearVisibilityAmount
+ return coordinator.linearVisibilityAmount
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 1adfef0..f98a88f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -68,9 +68,11 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
import com.android.systemui.statusbar.notification.row.NotificationGuts;
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository;
import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel;
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel;
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel;
import com.android.systemui.statusbar.notification.stack.PriorityBucket;
import com.android.systemui.util.ListenerSet;
@@ -97,7 +99,7 @@
* At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can
* clean this up in the future.
*/
-public final class NotificationEntry extends ListEntry {
+public final class NotificationEntry extends ListEntry implements NotificationRowRepository {
private final String mKey;
private StatusBarNotification mSbn;
@@ -159,6 +161,8 @@
StateFlowKt.MutableStateFlow(null);
private final MutableStateFlow<CharSequence> mHeadsUpStatusBarTextPublic =
StateFlowKt.MutableStateFlow(null);
+ private final MutableStateFlow<RichOngoingContentModel> mRichOngoingContentModel =
+ StateFlowKt.MutableStateFlow(null);
// indicates when this entry's view was first attached to a window
// this value will reset when the view is completely removed from the shade (ie: filtered out)
@@ -945,6 +949,7 @@
}
/** @see #setHeadsUpStatusBarText(CharSequence) */
+ @NonNull
public StateFlow<CharSequence> getHeadsUpStatusBarText() {
return mHeadsUpStatusBarText;
}
@@ -959,10 +964,17 @@
}
/** @see #setHeadsUpStatusBarTextPublic(CharSequence) */
+ @NonNull
public StateFlow<CharSequence> getHeadsUpStatusBarTextPublic() {
return mHeadsUpStatusBarTextPublic;
}
+ /** Gets the current RON content model, which may be null */
+ @NonNull
+ public StateFlow<RichOngoingContentModel> getRichOngoingContentModel() {
+ return mRichOngoingContentModel;
+ }
+
/**
* Sets the text to be displayed on the StatusBar, when this notification is the top pinned
* heads up, and its content is sensitive right now.
@@ -1047,6 +1059,7 @@
HeadsUpStatusBarModel headsUpStatusBarModel = contentModel.getHeadsUpStatusBarModel();
this.mHeadsUpStatusBarText.setValue(headsUpStatusBarModel.getPrivateText());
this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarModel.getPublicText());
+ this.mRichOngoingContentModel.setValue(contentModel.getRichOngoingContentModel());
}
/** Information about a suggestion that is being edited. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 4ba673d..05d71967 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -2950,24 +2950,21 @@
if (mShowingPublicInitialized && mShowingPublic == oldShowingPublic) {
return;
}
- float oldAlpha = getContentView().getAlpha();
if (!animated) {
- mPublicLayout.animate().cancel();
- mPrivateLayout.animate().cancel();
- if (mChildrenContainer != null) {
- mChildrenContainer.animate().cancel();
+ if (!NotificationContentAlphaOptimization.isEnabled()
+ || mShowingPublic != oldShowingPublic) {
+ // Don't reset the alpha or cancel the animation if the showing layout doesn't
+ // change
+ mPublicLayout.animate().cancel();
+ mPrivateLayout.animate().cancel();
+ if (mChildrenContainer != null) {
+ mChildrenContainer.animate().cancel();
+ }
+ resetAllContentAlphas();
}
- resetAllContentAlphas();
mPublicLayout.setVisibility(mShowingPublic ? View.VISIBLE : View.INVISIBLE);
updateChildrenVisibility();
- if (NotificationContentAlphaOptimization.isEnabled()) {
- // We want to set the old alpha to the now-showing layout to avoid breaking an
- // on-going animation
- if (oldAlpha != 1f) {
- setAlphaAndLayerType(mShowingPublic ? mPublicLayout : mPrivateLayout, oldAlpha);
- }
- }
} else {
animateShowingPublic(delay, duration, mShowingPublic);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 6f00d96..7fc331d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -72,6 +72,8 @@
import com.android.systemui.util.Compile;
import com.android.systemui.util.DumpUtilsKt;
+import kotlinx.coroutines.DisposableHandle;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -109,6 +111,8 @@
private View mHeadsUpChild;
private HybridNotificationView mSingleLineView;
+ @Nullable public DisposableHandle mContractedBinderHandle;
+
private RemoteInputView mExpandedRemoteInput;
private RemoteInputView mHeadsUpRemoteInput;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index e704140..492d802 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -46,6 +46,10 @@
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
@@ -62,6 +66,7 @@
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder
import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
@@ -86,6 +91,8 @@
private val remoteViewCache: NotifRemoteViewCache,
private val remoteInputManager: NotificationRemoteInputManager,
private val conversationProcessor: ConversationNotificationProcessor,
+ private val ronExtractor: RichOngoingNotificationContentExtractor,
+ private val ronInflater: RichOngoingNotificationViewInflater,
@NotifInflation private val inflationExecutor: Executor,
private val smartReplyStateInflater: SmartReplyStateInflater,
private val notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
@@ -133,6 +140,8 @@
remoteViewCache,
entry,
conversationProcessor,
+ ronExtractor,
+ ronInflater,
row,
bindParams.isMinimized,
bindParams.usesIncreasedHeight,
@@ -177,6 +186,7 @@
notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
headsUpStyleProvider = headsUpStyleProvider,
conversationProcessor = conversationProcessor,
+ ronExtractor = ronExtractor,
logger = logger,
)
inflateSmartReplyViews(
@@ -255,39 +265,31 @@
) {
when (inflateFlag) {
FLAG_CONTENT_VIEW_CONTRACTED ->
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
+ row.privateLayout.mContractedBinderHandle?.dispose()
+ row.privateLayout.mContractedBinderHandle = null
row.privateLayout.setContractedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
}
FLAG_CONTENT_VIEW_EXPANDED ->
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_EXPANDED
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) {
row.privateLayout.setExpandedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
}
FLAG_CONTENT_VIEW_HEADS_UP ->
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_HEADSUP
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) {
row.privateLayout.setHeadsUpChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
row.privateLayout.setHeadsUpInflatedSmartReplies(null)
}
FLAG_CONTENT_VIEW_PUBLIC ->
- row.publicLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ) {
+ row.publicLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
row.publicLayout.setContractedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)
}
FLAG_CONTENT_VIEW_SINGLE_LINE -> {
if (AsyncHybridViewInflation.isEnabled) {
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_SINGLELINE
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_SINGLELINE) {
row.privateLayout.setSingleLineView(null)
}
}
@@ -308,32 +310,22 @@
@InflationFlag contentViews: Int
) {
if (contentViews and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED)
}
if (contentViews and FLAG_CONTENT_VIEW_EXPANDED != 0) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_EXPANDED
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED)
}
if (contentViews and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_HEADSUP
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP)
}
if (contentViews and FLAG_CONTENT_VIEW_PUBLIC != 0) {
- row.publicLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- )
+ row.publicLayout.removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED)
}
if (
AsyncHybridViewInflation.isEnabled &&
contentViews and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_SINGLELINE
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE)
}
}
@@ -353,6 +345,8 @@
private val remoteViewCache: NotifRemoteViewCache,
private val entry: NotificationEntry,
private val conversationProcessor: ConversationNotificationProcessor,
+ private val ronExtractor: RichOngoingNotificationContentExtractor,
+ private val ronInflater: RichOngoingNotificationViewInflater,
private val row: ExpandableNotificationRow,
private val isMinimized: Boolean,
private val usesIncreasedHeight: Boolean,
@@ -432,6 +426,7 @@
notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
headsUpStyleProvider = headsUpStyleProvider,
conversationProcessor = conversationProcessor,
+ ronExtractor = ronExtractor,
logger = logger
)
logger.logAsyncTaskProgress(
@@ -463,6 +458,21 @@
)
}
}
+
+ if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
+ logger.logAsyncTaskProgress(entry, "inflating RON view")
+ inflationProgress.richOngoingNotificationViewHolder =
+ inflationProgress.contentModel.richOngoingContentModel?.let {
+ ronInflater.inflateView(
+ contentModel = it,
+ existingView = row.privateLayout.contractedChild,
+ entry = entry,
+ systemUiContext = context,
+ parentView = row.privateLayout
+ )
+ }
+ }
+
logger.logAsyncTaskProgress(entry, "getting row image resolver (on wrong thread!)")
val imageResolver = row.imageResolver
// wait for image resolver to finish preloading
@@ -568,6 +578,7 @@
var inflatedSmartReplyState: InflatedSmartReplyState? = null
var expandedInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
var headsUpInflatedSmartReplies: InflatedSmartReplyViewHolder? = null
+ var richOngoingNotificationViewHolder: InflatedContentViewHolder? = null
// Inflated SingleLineView that lacks the UI State
var inflatedSingleLineView: HybridNotificationView? = null
@@ -602,6 +613,7 @@
val inflateHeadsUp =
(reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 &&
result.remoteViews.headsUp != null)
+
if (inflateContracted || inflateExpanded || inflateHeadsUp) {
logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state")
result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry)
@@ -643,6 +655,7 @@
notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
headsUpStyleProvider: HeadsUpStyleProvider,
conversationProcessor: ConversationNotificationProcessor,
+ ronExtractor: RichOngoingNotificationContentExtractor,
logger: NotificationRowContentBinderLogger
): InflationProgress {
// process conversations and extract the messaging style
@@ -651,9 +664,24 @@
conversationProcessor.processNotification(entry, builder, logger)
} else null
+ val richOngoingContentModel =
+ if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
+ ronExtractor.extractContentModel(
+ entry = entry,
+ builder = builder,
+ systemUIContext = systemUIContext,
+ packageContext = packageContext
+ )
+ } else {
+ // if we're not re-inflating any RON views, make sure the model doesn't change
+ entry.richOngoingContentModel.value
+ }
+
+ val remoteViewsFlags = getRemoteViewsFlags(reInflateFlags, richOngoingContentModel)
+
val remoteViews =
createRemoteViews(
- reInflateFlags = reInflateFlags,
+ reInflateFlags = remoteViewsFlags,
builder = builder,
isMinimized = isMinimized,
usesIncreasedHeight = usesIncreasedHeight,
@@ -688,6 +716,7 @@
NotificationContentModel(
headsUpStatusBarModel = headsUpStatusBarModel,
singleLineViewModel = singleLineViewModel,
+ richOngoingContentModel = richOngoingContentModel,
)
return InflationProgress(
@@ -815,7 +844,7 @@
val publicLayout = row.publicLayout
val runningInflations = HashMap<Int, CancellationSignal>()
var flag = FLAG_CONTENT_VIEW_CONTRACTED
- if (reInflateFlags and flag != 0) {
+ if (reInflateFlags and flag != 0 && result.remoteViews.contracted != null) {
val isNewView =
!canReapplyRemoteView(
newView = result.remoteViews.contracted,
@@ -829,7 +858,7 @@
}
override val remoteView: RemoteViews
- get() = result.remoteViews.contracted!!
+ get() = result.remoteViews.contracted
}
logger.logAsyncTaskProgress(entry, "applying contracted view")
applyRemoteView(
@@ -847,104 +876,89 @@
callback = callback,
parentLayout = privateLayout,
existingView = privateLayout.contractedChild,
- existingWrapper =
- privateLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ),
+ existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_CONTRACTED),
runningInflations = runningInflations,
applyCallback = applyCallback,
logger = logger
)
}
flag = FLAG_CONTENT_VIEW_EXPANDED
- if (reInflateFlags and flag != 0) {
- if (result.remoteViews.expanded != null) {
- val isNewView =
- !canReapplyRemoteView(
- newView = result.remoteViews.expanded,
- oldView =
- remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
- )
- val applyCallback: ApplyCallback =
- object : ApplyCallback() {
- override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "expanded view applied")
- result.inflatedExpandedView = v
- }
-
- override val remoteView: RemoteViews
- get() = result.remoteViews.expanded
- }
- logger.logAsyncTaskProgress(entry, "applying expanded view")
- applyRemoteView(
- inflationExecutor = inflationExecutor,
- inflateSynchronously = inflateSynchronously,
- isMinimized = isMinimized,
- result = result,
- reInflateFlags = reInflateFlags,
- inflationId = flag,
- remoteViewCache = remoteViewCache,
- entry = entry,
- row = row,
- isNewView = isNewView,
- remoteViewClickHandler = remoteViewClickHandler,
- callback = callback,
- parentLayout = privateLayout,
- existingView = privateLayout.expandedChild,
- existingWrapper =
- privateLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_EXPANDED
- ),
- runningInflations = runningInflations,
- applyCallback = applyCallback,
- logger = logger
+ if (reInflateFlags and flag != 0 && result.remoteViews.expanded != null) {
+ val isNewView =
+ !canReapplyRemoteView(
+ newView = result.remoteViews.expanded,
+ oldView = remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
)
- }
+ val applyCallback: ApplyCallback =
+ object : ApplyCallback() {
+ override fun setResultView(v: View) {
+ logger.logAsyncTaskProgress(entry, "expanded view applied")
+ result.inflatedExpandedView = v
+ }
+
+ override val remoteView: RemoteViews
+ get() = result.remoteViews.expanded
+ }
+ logger.logAsyncTaskProgress(entry, "applying expanded view")
+ applyRemoteView(
+ inflationExecutor = inflationExecutor,
+ inflateSynchronously = inflateSynchronously,
+ isMinimized = isMinimized,
+ result = result,
+ reInflateFlags = reInflateFlags,
+ inflationId = flag,
+ remoteViewCache = remoteViewCache,
+ entry = entry,
+ row = row,
+ isNewView = isNewView,
+ remoteViewClickHandler = remoteViewClickHandler,
+ callback = callback,
+ parentLayout = privateLayout,
+ existingView = privateLayout.expandedChild,
+ existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED),
+ runningInflations = runningInflations,
+ applyCallback = applyCallback,
+ logger = logger
+ )
}
flag = FLAG_CONTENT_VIEW_HEADS_UP
- if (reInflateFlags and flag != 0) {
- if (result.remoteViews.headsUp != null) {
- val isNewView =
- !canReapplyRemoteView(
- newView = result.remoteViews.headsUp,
- oldView =
- remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
- )
- val applyCallback: ApplyCallback =
- object : ApplyCallback() {
- override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "heads up view applied")
- result.inflatedHeadsUpView = v
- }
-
- override val remoteView: RemoteViews
- get() = result.remoteViews.headsUp
- }
- logger.logAsyncTaskProgress(entry, "applying heads up view")
- applyRemoteView(
- inflationExecutor = inflationExecutor,
- inflateSynchronously = inflateSynchronously,
- isMinimized = isMinimized,
- result = result,
- reInflateFlags = reInflateFlags,
- inflationId = flag,
- remoteViewCache = remoteViewCache,
- entry = entry,
- row = row,
- isNewView = isNewView,
- remoteViewClickHandler = remoteViewClickHandler,
- callback = callback,
- parentLayout = privateLayout,
- existingView = privateLayout.headsUpChild,
- existingWrapper =
- privateLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_HEADSUP
- ),
- runningInflations = runningInflations,
- applyCallback = applyCallback,
- logger = logger
+ if (reInflateFlags and flag != 0 && result.remoteViews.headsUp != null) {
+ val isNewView =
+ !canReapplyRemoteView(
+ newView = result.remoteViews.headsUp,
+ oldView = remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
)
- }
+ val applyCallback: ApplyCallback =
+ object : ApplyCallback() {
+ override fun setResultView(v: View) {
+ logger.logAsyncTaskProgress(entry, "heads up view applied")
+ result.inflatedHeadsUpView = v
+ }
+
+ override val remoteView: RemoteViews
+ get() = result.remoteViews.headsUp
+ }
+ logger.logAsyncTaskProgress(entry, "applying heads up view")
+ applyRemoteView(
+ inflationExecutor = inflationExecutor,
+ inflateSynchronously = inflateSynchronously,
+ isMinimized = isMinimized,
+ result = result,
+ reInflateFlags = reInflateFlags,
+ inflationId = flag,
+ remoteViewCache = remoteViewCache,
+ entry = entry,
+ row = row,
+ isNewView = isNewView,
+ remoteViewClickHandler = remoteViewClickHandler,
+ callback = callback,
+ parentLayout = privateLayout,
+ existingView = privateLayout.headsUpChild,
+ existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP),
+ runningInflations = runningInflations,
+ applyCallback = applyCallback,
+ logger = logger
+ )
}
flag = FLAG_CONTENT_VIEW_PUBLIC
if (reInflateFlags and flag != 0) {
@@ -979,10 +993,7 @@
callback = callback,
parentLayout = publicLayout,
existingView = publicLayout.contractedChild,
- existingWrapper =
- publicLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ),
+ existingWrapper = publicLayout.getVisibleWrapper(VISIBLE_TYPE_CONTRACTED),
runningInflations = runningInflations,
applyCallback = applyCallback,
logger = logger
@@ -1359,79 +1370,34 @@
if (runningInflations.isNotEmpty()) {
return false
}
- val privateLayout = row.privateLayout
- val publicLayout = row.publicLayout
logger.logAsyncTaskProgress(entry, "finishing")
- if (reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
- if (result.inflatedContentView != null) {
- // New view case
- privateLayout.setContractedChild(result.inflatedContentView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_CONTRACTED,
- result.remoteViews.contracted
- )
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
- // Reinflation case. Only update if it's still cached (i.e. view has not been
- // freed while inflating).
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_CONTRACTED,
- result.remoteViews.contracted
- )
- }
+
+ // before updating the content model, stop existing binding if necessary
+ val hasRichOngoingContentModel = result.contentModel.richOngoingContentModel != null
+ val requestedRichOngoing = reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0
+ val rejectedRichOngoing = requestedRichOngoing && !hasRichOngoingContentModel
+ if (result.richOngoingNotificationViewHolder != null || rejectedRichOngoing) {
+ row.privateLayout.mContractedBinderHandle?.dispose()
+ row.privateLayout.mContractedBinderHandle = null
}
- if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
- if (result.inflatedExpandedView != null) {
- privateLayout.setExpandedChild(result.inflatedExpandedView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_EXPANDED,
- result.remoteViews.expanded
- )
- } else if (result.remoteViews.expanded == null) {
- privateLayout.setExpandedChild(null)
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_EXPANDED,
- result.remoteViews.expanded
- )
- }
- if (result.remoteViews.expanded != null) {
- privateLayout.setExpandedInflatedSmartReplies(
- result.expandedInflatedSmartReplies
- )
- } else {
- privateLayout.setExpandedInflatedSmartReplies(null)
- }
- row.setExpandable(result.remoteViews.expanded != null)
- }
- if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
- if (result.inflatedHeadsUpView != null) {
- privateLayout.setHeadsUpChild(result.inflatedHeadsUpView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_HEADS_UP,
- result.remoteViews.headsUp
- )
- } else if (result.remoteViews.headsUp == null) {
- privateLayout.setHeadsUpChild(null)
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_HEADS_UP,
- result.remoteViews.headsUp
- )
- }
- if (result.remoteViews.headsUp != null) {
- privateLayout.setHeadsUpInflatedSmartReplies(result.headsUpInflatedSmartReplies)
- } else {
- privateLayout.setHeadsUpInflatedSmartReplies(null)
- }
- }
+
+ // set the content model after disposal and before setting new rich ongoing view
+ entry.setContentModel(result.contentModel)
+ result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
+
+ // set normal remote views (skipping rich ongoing states when that model exists)
+ val remoteViewsFlags =
+ getRemoteViewsFlags(reInflateFlags, result.contentModel.richOngoingContentModel)
+ setContentViewsFromRemoteViews(
+ remoteViewsFlags,
+ entry,
+ remoteViewCache,
+ result,
+ row,
+ isMinimized,
+ )
+
+ // set single line view
if (
AsyncHybridViewInflation.isEnabled &&
reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
@@ -1444,80 +1410,144 @@
} else {
SingleLineViewBinder.bind(viewModel, singleLineView)
}
- privateLayout.setSingleLineView(result.inflatedSingleLineView)
+ row.privateLayout.setSingleLineView(result.inflatedSingleLineView)
}
}
- result.inflatedSmartReplyState?.let { privateLayout.setInflatedSmartReplyState(it) }
- if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) {
- if (result.inflatedPublicView != null) {
- publicLayout.setContractedChild(result.inflatedPublicView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_PUBLIC,
- result.remoteViews.public
- )
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_PUBLIC,
- result.remoteViews.public
- )
- }
+
+ // after updating the content model, set the view, then start the new binder
+ result.richOngoingNotificationViewHolder?.let { viewHolder ->
+ row.privateLayout.contractedChild = viewHolder.view
+ row.privateLayout.expandedChild = null
+ row.privateLayout.headsUpChild = null
+ row.privateLayout.setExpandedInflatedSmartReplies(null)
+ row.privateLayout.setHeadsUpInflatedSmartReplies(null)
+ row.privateLayout.mContractedBinderHandle =
+ viewHolder.binder.setupContentViewBinder()
+ row.setExpandable(false)
+ remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
+ remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
+ remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
}
- if (AsyncGroupHeaderViewInflation.isEnabled) {
- if (reInflateFlags and FLAG_GROUP_SUMMARY_HEADER != 0) {
- if (result.inflatedGroupHeaderView != null) {
- // We need to set if the row is minimized before setting the group header to
- // make sure the setting of header view works correctly
- row.setIsMinimized(isMinimized)
- row.setGroupHeader(/* headerView= */ result.inflatedGroupHeaderView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_GROUP_SUMMARY_HEADER,
- result.remoteViews.normalGroupHeader
- )
- } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) {
- // Re-inflation case. Only update if it's still cached (i.e. view has not
- // been freed while inflating).
- remoteViewCache.putCachedView(
- entry,
- FLAG_GROUP_SUMMARY_HEADER,
- result.remoteViews.normalGroupHeader
- )
- }
- }
- if (reInflateFlags and FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER != 0) {
- if (result.inflatedMinimizedGroupHeaderView != null) {
- // We need to set if the row is minimized before setting the group header to
- // make sure the setting of header view works correctly
- row.setIsMinimized(isMinimized)
- row.setMinimizedGroupHeader(
- /* headerView= */ result.inflatedMinimizedGroupHeaderView
- )
- remoteViewCache.putCachedView(
- entry,
- FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
- result.remoteViews.minimizedGroupHeader
- )
- } else if (
- remoteViewCache.hasCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)
- ) {
- // Re-inflation case. Only update if it's still cached (i.e. view has not
- // been freed while inflating).
- remoteViewCache.putCachedView(
- entry,
- FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
- result.remoteViews.normalGroupHeader
- )
- }
- }
- }
- entry.setContentModel(result.contentModel)
+
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
endListener?.onAsyncInflationFinished(entry)
return true
}
+ private fun setContentViewsFromRemoteViews(
+ @InflationFlag reInflateFlags: Int,
+ entry: NotificationEntry,
+ remoteViewCache: NotifRemoteViewCache,
+ result: InflationProgress,
+ row: ExpandableNotificationRow,
+ isMinimized: Boolean,
+ ) {
+ val privateLayout = row.privateLayout
+ val publicLayout = row.publicLayout
+ val remoteViewsUpdater = RemoteViewsUpdater(reInflateFlags, entry, remoteViewCache)
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_CONTRACTED,
+ result.remoteViews.contracted,
+ result.inflatedContentView,
+ privateLayout::setContractedChild
+ )
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_EXPANDED,
+ result.remoteViews.expanded,
+ result.inflatedExpandedView,
+ privateLayout::setExpandedChild
+ )
+ remoteViewsUpdater.setSmartReplies(
+ FLAG_CONTENT_VIEW_EXPANDED,
+ result.remoteViews.expanded,
+ result.expandedInflatedSmartReplies,
+ privateLayout::setExpandedInflatedSmartReplies
+ )
+ if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
+ row.setExpandable(result.remoteViews.expanded != null)
+ }
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_HEADS_UP,
+ result.remoteViews.headsUp,
+ result.inflatedHeadsUpView,
+ privateLayout::setHeadsUpChild
+ )
+ remoteViewsUpdater.setSmartReplies(
+ FLAG_CONTENT_VIEW_HEADS_UP,
+ result.remoteViews.headsUp,
+ result.headsUpInflatedSmartReplies,
+ privateLayout::setHeadsUpInflatedSmartReplies
+ )
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_PUBLIC,
+ result.remoteViews.public,
+ result.inflatedPublicView,
+ publicLayout::setContractedChild
+ )
+ if (AsyncGroupHeaderViewInflation.isEnabled) {
+ remoteViewsUpdater.setContentView(
+ FLAG_GROUP_SUMMARY_HEADER,
+ result.remoteViews.normalGroupHeader,
+ result.inflatedGroupHeaderView,
+ ) { views ->
+ row.setIsMinimized(isMinimized)
+ row.setGroupHeader(views)
+ }
+ remoteViewsUpdater.setContentView(
+ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
+ result.remoteViews.minimizedGroupHeader,
+ result.inflatedMinimizedGroupHeaderView,
+ ) { views ->
+ row.setIsMinimized(isMinimized)
+ row.setMinimizedGroupHeader(views)
+ }
+ }
+ }
+
+ private class RemoteViewsUpdater(
+ @InflationFlag private val reInflateFlags: Int,
+ private val entry: NotificationEntry,
+ private val remoteViewCache: NotifRemoteViewCache,
+ ) {
+ fun <V : View> setContentView(
+ @InflationFlag flagState: Int,
+ remoteViews: RemoteViews?,
+ view: V?,
+ setView: (V?) -> Unit,
+ ) {
+ val clearViewFlags = FLAG_CONTENT_VIEW_HEADS_UP or FLAG_CONTENT_VIEW_EXPANDED
+ val shouldClearView = flagState and clearViewFlags != 0
+ if (reInflateFlags and flagState != 0) {
+ if (view != null) {
+ setView(view)
+ remoteViewCache.putCachedView(entry, flagState, remoteViews)
+ } else if (shouldClearView && remoteViews == null) {
+ setView(null)
+ remoteViewCache.removeCachedView(entry, flagState)
+ } else if (remoteViewCache.hasCachedView(entry, flagState)) {
+ // Re-inflation case. Only update if it's still cached (i.e. view has not
+ // been freed while inflating).
+ remoteViewCache.putCachedView(entry, flagState, remoteViews)
+ }
+ }
+ }
+
+ fun setSmartReplies(
+ @InflationFlag flagState: Int,
+ remoteViews: RemoteViews?,
+ smartReplies: InflatedSmartReplyViewHolder?,
+ setSmartReplies: (InflatedSmartReplyViewHolder?) -> Unit,
+ ) {
+ if (reInflateFlags and flagState != 0) {
+ if (remoteViews != null) {
+ setSmartReplies(smartReplies)
+ } else {
+ setSmartReplies(null)
+ }
+ }
+ }
+ }
+
private fun createExpandedView(
builder: Notification.Builder,
isMinimized: Boolean
@@ -1562,6 +1592,21 @@
!oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)
}
+ @InflationFlag
+ private fun getRemoteViewsFlags(
+ @InflationFlag reInflateFlags: Int,
+ richOngoingContentModel: RichOngoingContentModel?
+ ): Int =
+ if (richOngoingContentModel != null) {
+ reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING.inv()
+ } else {
+ reInflateFlags
+ }
+
+ @InflationFlag
+ private const val CONTENT_VIEWS_TO_CREATE_RICH_ONGOING =
+ FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP
+
private const val ASYNC_TASK_TRACE_METHOD =
"NotificationRowContentBinderImpl.AsyncInflationTask"
private const val APPLY_TRACE_METHOD = "NotificationRowContentBinderImpl#apply"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index 84f2f66..c630c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -18,6 +18,8 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent;
import dagger.Binds;
import dagger.Module;
@@ -28,7 +30,7 @@
/**
* Dagger Module containing notification row and view inflation implementations.
*/
-@Module
+@Module(subcomponents = {RichOngoingViewModelComponent.class})
public abstract class NotificationRowModule {
/**
@@ -47,6 +49,25 @@
}
}
+ /** Provides ron content model extractor. */
+ @Provides
+ @SysUISingleton
+ public static RichOngoingNotificationContentExtractor provideRonContentExtractor(
+ Provider<RichOngoingNotificationContentExtractorImpl> realImpl
+ ) {
+ if (RichOngoingNotificationFlag.isEnabled()) {
+ return realImpl.get();
+ } else {
+ return new NoOpRichOngoingNotificationContentExtractor();
+ }
+ }
+
+ /** Provides ron view inflater. */
+ @Binds
+ @SysUISingleton
+ public abstract RichOngoingNotificationViewInflater provideRonViewInflater(
+ RichOngoingNotificationViewInflaterImpl impl);
+
/**
* Provides notification remote view cache instance.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
new file mode 100644
index 0000000..b5ea861
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.statusbar.notification.row
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.Context
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.shared.IconModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneId
+import javax.inject.Inject
+
+/**
+ * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
+ * applicable to the given style.
+ */
+interface RichOngoingNotificationContentExtractor {
+ fun extractContentModel(
+ entry: NotificationEntry,
+ builder: Notification.Builder,
+ systemUIContext: Context,
+ packageContext: Context
+ ): RichOngoingContentModel?
+}
+
+class NoOpRichOngoingNotificationContentExtractor : RichOngoingNotificationContentExtractor {
+ override fun extractContentModel(
+ entry: NotificationEntry,
+ builder: Notification.Builder,
+ systemUIContext: Context,
+ packageContext: Context
+ ): RichOngoingContentModel? = null
+}
+
+@SysUISingleton
+class RichOngoingNotificationContentExtractorImpl @Inject constructor() :
+ RichOngoingNotificationContentExtractor {
+
+ init {
+ /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
+ }
+
+ override fun extractContentModel(
+ entry: NotificationEntry,
+ builder: Notification.Builder,
+ systemUIContext: Context,
+ packageContext: Context
+ ): RichOngoingContentModel? =
+ try {
+ val sbn = entry.sbn
+ val notification = sbn.notification
+ val icon = IconModel(notification.smallIcon)
+ if (sbn.packageName == "com.google.android.deskclock") {
+ when (notification.channelId) {
+ "Timers v2" -> {
+ parseTimerNotification(notification, icon)
+ }
+ "Stopwatch v2" -> {
+ Log.i("RONs", "Can't process stopwatch yet")
+ null
+ }
+ else -> {
+ Log.i("RONs", "Can't process channel '${notification.channelId}'")
+ null
+ }
+ }
+ } else null
+ } catch (e: Exception) {
+ Log.e("RONs", "Error parsing RON", e)
+ null
+ }
+
+ /**
+ * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available
+ * inside the sortKey of the clock app's timer notifications.
+ */
+ private fun parseTimerNotification(
+ notification: Notification,
+ icon: IconModel
+ ): TimerContentModel {
+ // sortKey=1 0|↺7|RUNNING|▶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57
+ // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06
+ // sortKey=1 1|↺7|RUNNING|▶16:30:28.433|Σ0:04:05|Δ0:00:06|⏳0:03:59
+ // sortKey=1 0|↺7|RUNNING|▶16:36:18.350|Σ0:05:00|Δ0:01:42|⏳0:03:18
+ // sortKey=1 2|↺7|RUNNING|▶16:38:37.816|Σ0:02:00|Δ0:01:09|⏳0:00:51
+ // ▶ = "current" time (when updated)
+ // Σ = total time
+ // Δ = time elapsed
+ // ⏳ = time remaining
+ val sortKey = notification.sortKey
+ val (_, _, state, extra) = sortKey.split("|", limit = 4)
+ return when (state) {
+ "PAUSED" -> {
+ val (total, _, remaining) = extra.split("|")
+ val timeRemaining = parseTimeDelta(remaining)
+ TimerContentModel(
+ icon = icon,
+ name = total,
+ state =
+ TimerContentModel.TimerState.Paused(
+ timeRemaining = timeRemaining,
+ resumeIntent = notification.findActionWithName("Resume"),
+ resetIntent = notification.findActionWithName("Reset"),
+ )
+ )
+ }
+ "RUNNING" -> {
+ val (current, total, _, remaining) = extra.split("|")
+ val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
+ TimerContentModel(
+ icon = icon,
+ name = total,
+ state =
+ TimerContentModel.TimerState.Running(
+ finishTime = finishTime,
+ pauseIntent = notification.findActionWithName("Pause"),
+ addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
+ )
+ )
+ }
+ else -> error("unknown state ($state) in sortKey=$sortKey")
+ }
+ }
+
+ private fun Notification.findActionWithName(name: String): PendingIntent? {
+ return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
+ }
+
+ private fun parseCurrentTime(current: String): Long {
+ val (hour, minute, second, millis) = current.replace("▶", "").split(":", ".")
+ // NOTE: this won't work correctly at/around midnight. It's just for prototyping.
+ val localDateTime =
+ LocalDateTime.of(
+ LocalDate.now(),
+ LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000)
+ )
+ val offset = ZoneId.systemDefault().rules.getOffset(localDateTime)
+ return localDateTime.toInstant(offset).toEpochMilli()
+ }
+
+ private fun parseTimeDelta(delta: String): Duration {
+ val (hour, minute, second) = delta.replace("Σ", "").replace("⏳", "").split(":")
+ return Duration.ofHours(hour.toLong())
+ .plusMinutes(minute.toLong())
+ .plusSeconds(second.toLong())
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
new file mode 100644
index 0000000..e9c4960
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.statusbar.notification.row
+
+import android.app.Notification
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.StopwatchContentModel
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import com.android.systemui.statusbar.notification.row.ui.view.TimerView
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.TimerViewBinder
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+
+fun interface DeferredContentViewBinder {
+ fun setupContentViewBinder(): DisposableHandle
+}
+
+class InflatedContentViewHolder(val view: View, val binder: DeferredContentViewBinder)
+
+/**
+ * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
+ * applicable to the given style.
+ */
+interface RichOngoingNotificationViewInflater {
+ fun inflateView(
+ contentModel: RichOngoingContentModel,
+ existingView: View?,
+ entry: NotificationEntry,
+ systemUiContext: Context,
+ parentView: ViewGroup,
+ ): InflatedContentViewHolder?
+}
+
+@SysUISingleton
+class RichOngoingNotificationViewInflaterImpl
+@Inject
+constructor(
+ private val viewModelComponentFactory: RichOngoingViewModelComponent.Factory,
+) : RichOngoingNotificationViewInflater {
+
+ override fun inflateView(
+ contentModel: RichOngoingContentModel,
+ existingView: View?,
+ entry: NotificationEntry,
+ systemUiContext: Context,
+ parentView: ViewGroup,
+ ): InflatedContentViewHolder? {
+ if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return null
+ val component = viewModelComponentFactory.create(entry)
+ return when (contentModel) {
+ is TimerContentModel ->
+ inflateTimerView(
+ existingView,
+ component::createTimerViewModel,
+ systemUiContext,
+ parentView
+ )
+ is StopwatchContentModel -> TODO("Not yet implemented")
+ }
+ }
+
+ private fun inflateTimerView(
+ existingView: View?,
+ createViewModel: () -> TimerViewModel,
+ systemUiContext: Context,
+ parentView: ViewGroup,
+ ): InflatedContentViewHolder? {
+ if (existingView is TimerView && !existingView.isReinflateNeeded()) return null
+ val newView =
+ LayoutInflater.from(systemUiContext)
+ .inflate(
+ R.layout.rich_ongoing_timer_notification,
+ parentView,
+ /* attachToRoot= */ false
+ ) as TimerView
+ return InflatedContentViewHolder(newView) {
+ TimerViewBinder.bindWhileAttached(newView, createViewModel())
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt
new file mode 100644
index 0000000..bac887b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.statusbar.notification.row.data.repository
+
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import kotlinx.coroutines.flow.StateFlow
+
+/** A repository of states relating to a specific notification row. */
+interface NotificationRowRepository {
+ /**
+ * A flow of an immutable data class with the current state of the Rich Ongoing Notification
+ * content, if applicable.
+ */
+ val richOngoingContentModel: StateFlow<RichOngoingContentModel?>
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
new file mode 100644
index 0000000..4705ace
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.statusbar.notification.row.domain.interactor
+
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterIsInstance
+
+/** Interactor specific to a particular notification row. */
+class NotificationRowInteractor @Inject constructor(repository: NotificationRowRepository) {
+ /** Content of a rich ongoing timer notification. */
+ val timerContentModel: Flow<TimerContentModel> =
+ repository.richOngoingContentModel.filterIsInstance<TimerContentModel>()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt
new file mode 100644
index 0000000..e611938
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/IconModel.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.statusbar.notification.row.shared
+
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+
+// TODO: figure out how to support lazy resolution of the drawable, e.g. on unrelated text change
+class IconModel(val icon: Icon) {
+ var drawable: Drawable? = null
+
+ override fun equals(other: Any?): Boolean =
+ when (other) {
+ null -> false
+ (other === this) -> true
+ !is IconModel -> false
+ else -> other.icon.sameAs(icon)
+ }
+
+ override fun toString(): String = "IconModel(icon=$icon, drawable=$drawable)"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
index b2421bc..46010a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/NotificationContentModel.kt
@@ -21,4 +21,7 @@
data class NotificationContentModel(
val headsUpStatusBarModel: HeadsUpStatusBarModel,
val singleLineViewModel: SingleLineViewModel? = null,
+ val richOngoingContentModel: RichOngoingContentModel? = null,
)
+
+sealed interface RichOngoingContentModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
new file mode 100644
index 0000000..5584701
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.statusbar.notification.row.shared
+
+import android.app.PendingIntent
+import java.time.Duration
+
+/**
+ * Represents a simple timer that counts down to a time.
+ *
+ * @param name the label for the timer
+ * @param state state of the timer, including time and whether it is paused or running
+ */
+data class TimerContentModel(
+ val icon: IconModel,
+ val name: String,
+ val state: TimerState,
+) : RichOngoingContentModel {
+ /** The state (paused or running) of the timer, and relevant time */
+ sealed interface TimerState {
+ /**
+ * Indicates a running timer
+ *
+ * @param finishTime the time in ms since epoch that the timer will finish
+ * @param pauseIntent the action for pausing the timer
+ */
+ data class Running(
+ val finishTime: Long,
+ val pauseIntent: PendingIntent?,
+ val addOneMinuteIntent: PendingIntent?,
+ ) : TimerState
+
+ /**
+ * Indicates a paused timer
+ *
+ * @param timeRemaining the time in ms remaining on the paused timer
+ * @param resumeIntent the action for resuming the timer
+ */
+ data class Paused(
+ val timeRemaining: Duration,
+ val resumeIntent: PendingIntent?,
+ val resetIntent: PendingIntent?,
+ ) : TimerState
+ }
+}
+
+/**
+ * Represents a simple stopwatch that counts up and allows tracking laps.
+ *
+ * @param state state of the stopwatch, including time and whether it is paused or running
+ * @param lapDurations a list of durations of each completed lap
+ */
+data class StopwatchContentModel(
+ val icon: IconModel,
+ val state: StopwatchState,
+ val lapDurations: List<Long>,
+) : RichOngoingContentModel {
+ /** The state (paused or running) of the stopwatch, and relevant time */
+ sealed interface StopwatchState {
+ /**
+ * Indicates a running stopwatch
+ *
+ * @param startTime the time in ms since epoch that the stopwatch started, plus any
+ * accumulated pause time
+ * @param pauseIntent the action for pausing the stopwatch
+ */
+ data class Running(
+ val startTime: Long,
+ val pauseIntent: PendingIntent,
+ ) : StopwatchState
+
+ /**
+ * Indicates a paused stopwatch
+ *
+ * @param timeElapsed the time in ms elapsed on the stopwatch
+ * @param resumeIntent the action for resuming the stopwatch
+ */
+ data class Paused(
+ val timeElapsed: Duration,
+ val resumeIntent: PendingIntent,
+ ) : StopwatchState
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt
new file mode 100644
index 0000000..4a7f7cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingNotificationFlag.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.statusbar.notification.row.shared
+
+import android.app.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the api rich ongoing flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object RichOngoingNotificationFlag {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_API_RICH_ONGOING
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.apiRichOngoing()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.kt
new file mode 100644
index 0000000..95c507c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/ConfigurationTracker.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.statusbar.notification.row.ui.view
+
+import android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS
+import android.content.pm.ActivityInfo.CONFIG_DENSITY
+import android.content.pm.ActivityInfo.CONFIG_FONT_SCALE
+import android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION
+import android.content.pm.ActivityInfo.CONFIG_LOCALE
+import android.content.pm.ActivityInfo.CONFIG_UI_MODE
+import android.content.res.Configuration
+import android.content.res.Resources
+
+/**
+ * Tracks the active configuration when constructed and returns (when queried) whether the
+ * configuration has unhandled changes.
+ */
+class ConfigurationTracker(
+ private val resources: Resources,
+ private val unhandledConfigChanges: Int
+) {
+ private val initialConfig = Configuration(resources.configuration)
+
+ constructor(
+ resources: Resources,
+ handlesDensityFontScale: Boolean = false,
+ handlesTheme: Boolean = false,
+ handlesLocaleAndLayout: Boolean = true,
+ ) : this(
+ resources,
+ unhandledConfigChanges =
+ (if (handlesDensityFontScale) 0 else CONFIG_DENSITY or CONFIG_FONT_SCALE) or
+ (if (handlesTheme) 0 else CONFIG_ASSETS_PATHS or CONFIG_UI_MODE) or
+ (if (handlesLocaleAndLayout) 0 else CONFIG_LOCALE or CONFIG_LAYOUT_DIRECTION)
+ )
+
+ /**
+ * Whether the current configuration has unhandled changes relative to the initial configuration
+ */
+ fun hasUnhandledConfigChange(): Boolean =
+ initialConfig.diff(resources.configuration) and unhandledConfigChanges != 0
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
new file mode 100644
index 0000000..0d83ace
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.statusbar.notification.row.ui.view
+
+import android.annotation.DrawableRes
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.Button
+
+class TimerButtonView
+@JvmOverloads
+constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : Button(context, attrs, defStyleAttr, defStyleRes) {
+
+ private val Int.dp: Int
+ get() = (this * context.resources.displayMetrics.density).toInt()
+
+ fun setIcon(@DrawableRes icon: Int) {
+ val drawable = context.getDrawable(icon)
+ drawable?.setBounds(0, 0, 24.dp, 24.dp)
+ setCompoundDrawablesRelative(drawable, null, null, null)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
new file mode 100644
index 0000000..2e164d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.statusbar.notification.row.ui.view
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.widget.Chronometer
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isVisible
+import com.android.systemui.res.R
+
+class TimerView
+@JvmOverloads
+constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0,
+) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+ private val configTracker = ConfigurationTracker(resources)
+
+ private lateinit var icon: ImageView
+ private lateinit var label: TextView
+ private lateinit var chronometer: Chronometer
+ private lateinit var pausedTimeRemaining: TextView
+ lateinit var mainButton: TimerButtonView
+ private set
+
+ lateinit var altButton: TimerButtonView
+ private set
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ icon = requireViewById(R.id.icon)
+ label = requireViewById(R.id.label)
+ chronometer = requireViewById(R.id.chronoRemaining)
+ pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
+ mainButton = requireViewById(R.id.mainButton)
+ altButton = requireViewById(R.id.altButton)
+ }
+
+ /** the resources configuration has changed such that the view needs to be reinflated */
+ fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
+
+ fun setIcon(iconDrawable: Drawable?) {
+ this.icon.setImageDrawable(iconDrawable)
+ }
+
+ fun setLabel(label: String) {
+ this.label.text = label
+ }
+
+ fun setPausedTime(pausedTime: String?) {
+ if (pausedTime != null) {
+ pausedTimeRemaining.text = pausedTime
+ pausedTimeRemaining.isVisible = true
+ } else {
+ pausedTimeRemaining.isVisible = false
+ }
+ }
+
+ fun setCountdownTime(countdownTimeMs: Long?) {
+ if (countdownTimeMs != null) {
+ chronometer.base =
+ countdownTimeMs - System.currentTimeMillis() + SystemClock.elapsedRealtime()
+ chronometer.isVisible = true
+ chronometer.start()
+ } else {
+ chronometer.isVisible = false
+ chronometer.stop()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
new file mode 100644
index 0000000..c9ff589
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.statusbar.notification.row.ui.viewbinder
+
+import android.view.View
+import androidx.core.view.isGone
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+import com.android.systemui.statusbar.notification.row.ui.view.TimerView
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/** Binds a [TimerView] to its [view model][TimerViewModel]. */
+object TimerViewBinder {
+ fun bindWhileAttached(
+ view: TimerView,
+ viewModel: TimerViewModel,
+ ): DisposableHandle {
+ return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } }
+ }
+
+ suspend fun bind(
+ view: TimerView,
+ viewModel: TimerViewModel,
+ ) = coroutineScope {
+ launch { viewModel.icon.collect { view.setIcon(it) } }
+ launch { viewModel.label.collect { view.setLabel(it) } }
+ launch { viewModel.pausedTime.collect { view.setPausedTime(it) } }
+ launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
+ launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
+ launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
+ }
+
+ fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
+ if (model != null) {
+ buttonView.setIcon(model.iconRes)
+ buttonView.setText(model.labelRes)
+ buttonView.setOnClickListener(
+ model.pendingIntent?.let { pendingIntent ->
+ View.OnClickListener { pendingIntent.send() }
+ }
+ )
+ buttonView.isEnabled = model.pendingIntent != null
+ }
+ buttonView.isGone = model == null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
new file mode 100644
index 0000000..dad52a3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.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.statusbar.notification.row.ui.viewmodel
+
+// noinspection CleanArchitectureDependencyViolation
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent
+interface RichOngoingViewModelComponent {
+
+ @Subcomponent.Factory
+ interface Factory {
+ /** Creates an instance of [RichOngoingViewModelComponent]. */
+ fun create(
+ @BindsInstance repository: NotificationRowRepository
+ ): RichOngoingViewModelComponent
+ }
+
+ fun createTimerViewModel(): TimerViewModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
new file mode 100644
index 0000000..a85c87f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.statusbar.notification.row.ui.viewmodel
+
+import android.annotation.DrawableRes
+import android.annotation.StringRes
+import android.app.PendingIntent
+import android.graphics.drawable.Drawable
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel.TimerState
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import java.time.Duration
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+
+/** A view model for Timer notifications. */
+class TimerViewModel
+@Inject
+constructor(
+ dumpManager: DumpManager,
+ rowInteractor: NotificationRowInteractor,
+) : FlowDumperImpl(dumpManager) {
+ init {
+ /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
+ }
+
+ private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
+
+ val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable }
+
+ val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
+
+ val countdownTime: Flow<Long?> = state.map { (it as? TimerState.Running)?.finishTime }
+
+ val pausedTime: Flow<String?> =
+ state.map { (it as? TimerState.Paused)?.timeRemaining?.format() }
+
+ val mainButtonModel: Flow<ButtonViewModel> =
+ state.map {
+ when (it) {
+ is TimerState.Paused ->
+ ButtonViewModel(
+ it.resumeIntent,
+ com.android.systemui.res.R.string.controls_media_resume, // "Resume",
+ com.android.systemui.res.R.drawable.ic_media_play
+ )
+ is TimerState.Running ->
+ ButtonViewModel(
+ it.pauseIntent,
+ com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
+ com.android.systemui.res.R.drawable.ic_media_pause
+ )
+ }
+ }
+
+ val altButtonModel: Flow<ButtonViewModel?> =
+ state.map {
+ when (it) {
+ is TimerState.Paused ->
+ it.resetIntent?.let { resetIntent ->
+ ButtonViewModel(
+ resetIntent,
+ com.android.systemui.res.R.string.reset, // "Reset",
+ com.android.systemui.res.R.drawable.ic_close_white_rounded
+ )
+ }
+ is TimerState.Running ->
+ it.addOneMinuteIntent?.let { addOneMinuteIntent ->
+ ButtonViewModel(
+ addOneMinuteIntent,
+ com.android.systemui.res.R.string.add, // "Add 1 minute",
+ com.android.systemui.res.R.drawable.ic_add
+ )
+ }
+ }
+ }
+
+ data class ButtonViewModel(
+ val pendingIntent: PendingIntent?,
+ @StringRes val labelRes: Int,
+ @DrawableRes val iconRes: Int,
+ )
+}
+
+private fun Duration.format(): String {
+ val hours = this.toHours()
+ return if (hours > 0) {
+ String.format("%d:%02d:%02d", hours, toMinutesPart(), toSecondsPart())
+ } else {
+ String.format("%d:%02d", toMinutes(), toSecondsPart())
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 2cbb6ae..fbddc06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -131,13 +131,13 @@
/** Distance of top of notifications panel from top of screen. */
private float mStackY = 0;
- /** Height of notifications panel. */
+ /** Height of notifications panel interpolated by the expansion fraction. */
private float mStackHeight = 0;
/** Fraction of shade expansion. */
private float mExpansionFraction;
- /** Height of the notifications panel without top padding when expansion completes. */
+ /** Height of the notifications panel when expansion completes. */
private float mStackEndHeight;
/** Whether we are swiping up. */
@@ -176,8 +176,7 @@
}
/**
- * @param stackEndHeight Height of the notifications panel without top padding
- * when expansion completes.
+ * @see #getStackEndHeight()
*/
public void setStackEndHeight(float stackEndHeight) {
mStackEndHeight = stackEndHeight;
@@ -257,14 +256,14 @@
}
/**
- * @param stackHeight Height of notifications panel.
+ * @see #getStackHeight()
*/
public void setStackHeight(float stackHeight) {
mStackHeight = stackHeight;
}
/**
- * @return Height of notifications panel.
+ * @return Height of notifications panel interpolated by the expansion fraction.
*/
public float getStackHeight() {
return mStackHeight;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 1eee466..d54e66e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1440,22 +1440,40 @@
@VisibleForTesting
public void updateStackEndHeightAndStackHeight(float fraction) {
final float oldStackHeight = mAmbientState.getStackHeight();
- if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) {
- final float endHeight = updateStackEndHeight(
- getHeight(), getEmptyBottomMargin(), getTopPadding());
+ if (SceneContainerFlag.isEnabled()) {
+ final float endHeight;
+ if (!shouldSkipHeightUpdate()) {
+ endHeight = updateStackEndHeight();
+ } else {
+ endHeight = mAmbientState.getStackEndHeight();
+ }
updateStackHeight(endHeight, fraction);
} else {
- // Always updateStackHeight to prevent jumps in the stack height when this fraction
- // suddenly reapplies after a freeze.
- final float endHeight = mAmbientState.getStackEndHeight();
- updateStackHeight(endHeight, fraction);
+ if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) {
+ final float endHeight = updateStackEndHeight(
+ getHeight(), getEmptyBottomMargin(), getTopPadding());
+ updateStackHeight(endHeight, fraction);
+ } else {
+ // Always updateStackHeight to prevent jumps in the stack height when this fraction
+ // suddenly reapplies after a freeze.
+ final float endHeight = mAmbientState.getStackEndHeight();
+ updateStackHeight(endHeight, fraction);
+ }
}
if (oldStackHeight != mAmbientState.getStackHeight()) {
requestChildrenUpdate();
}
}
+ private float updateStackEndHeight() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ float height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
+ mAmbientState.setStackEndHeight(height);
+ return height;
+ }
+
private float updateStackEndHeight(float height, float bottomMargin, float topPadding) {
+ SceneContainerFlag.assertInLegacyMode();
final float stackEndHeight;
if (mMaxDisplayedNotifications != -1) {
// The stack intrinsic height already contains the correct value when there is a limit
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index ca74c0e..ee7b5c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -445,12 +445,26 @@
state.visibleChildren.clear();
state.visibleChildren.ensureCapacity(childCount);
int notGoneIndex = 0;
+ boolean emptyShadeVisible = false;
for (int i = 0; i < childCount; i++) {
ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
if (v.getVisibility() != View.GONE) {
if (v == ambientState.getShelf()) {
continue;
}
+ if (FooterViewRefactor.isEnabled()) {
+ if (v instanceof EmptyShadeView) {
+ emptyShadeVisible = true;
+ }
+ if (v instanceof FooterView) {
+ if (emptyShadeVisible || notGoneIndex == 0) {
+ // if the empty shade is visible or the footer is the first visible
+ // view, we're in a transitory state so let's leave the footer alone.
+ continue;
+ }
+ }
+ }
+
notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
if (v instanceof ExpandableNotificationRow row) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index 639560f..e96326a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -34,6 +34,7 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.Flags.communalHub
+import com.android.systemui.Flags.mediaLockscreenLaunchAnimation
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.DelegateTransitionAnimatorController
import com.android.systemui.assist.AssistManager
@@ -635,8 +636,9 @@
isActivityIntent: Boolean,
showOverLockscreen: Boolean,
): Boolean {
- // TODO(b/294418322): Support launch animations when occluded.
- if (keyguardStateController.isOccluded) {
+ // TODO(b/294418322): always support launch animations when occluded.
+ val ignoreOcclusion = showOverLockscreen && mediaLockscreenLaunchAnimation()
+ if (keyguardStateController.isOccluded && !ignoreOcclusion) {
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 2371eed..3ba62b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -44,6 +44,7 @@
import androidx.lifecycle.Observer;
+import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
@@ -57,6 +58,7 @@
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.res.R;
import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
@@ -354,7 +356,11 @@
mProvisionedController.addCallback(this);
mCurrentUserSetup = mProvisionedController.isCurrentUserSetup();
mZenController.addCallback(this);
- mCast.addCallback(mCastCallback);
+ if (!Flags.statusBarScreenSharingChips()) {
+ // If the flag is enabled, the cast icon is handled in the new screen sharing chips
+ // instead of here so we don't need to listen for events here.
+ mCast.addCallback(mCastCallback);
+ }
mHotspot.addCallback(mHotspotCallback);
mNextAlarmController.addCallback(mNextAlarmCallback);
mDataSaver.addCallback(this);
@@ -362,7 +368,11 @@
mPrivacyItemController.addCallback(this);
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
mLocationController.addCallback(this);
- mRecordingController.addCallback(this);
+ if (!Flags.statusBarScreenSharingChips()) {
+ // If the flag is enabled, the screen record icon is handled in the new screen sharing
+ // chips instead of here so we don't need to listen for events here.
+ mRecordingController.addCallback(this);
+ }
mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(),
this::onConnectedDisplayAvailabilityChanged);
@@ -519,6 +529,11 @@
}
private void updateCast() {
+ if (Flags.statusBarScreenSharingChips()) {
+ // The cast icon is handled in the new screen sharing chips instead of here.
+ return;
+ }
+
boolean isCasting = false;
for (CastDevice device : mCast.getCastDevices()) {
if (device.isCasting()) {
@@ -788,6 +803,10 @@
private Runnable mRemoveCastIconRunnable = new Runnable() {
@Override
public void run() {
+ if (Flags.statusBarScreenSharingChips()) {
+ // The cast icon is handled in the new screen sharing chips instead of here.
+ return;
+ }
if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW");
mIconController.setIconVisibility(mSlotCast, false);
}
@@ -796,8 +815,13 @@
// Screen Recording
@Override
public void onCountdown(long millisUntilFinished) {
+ if (Flags.statusBarScreenSharingChips()) {
+ // The screen record icon is handled in the new screen sharing chips instead of here.
+ return;
+ }
if (DEBUG) Log.d(TAG, "screenrecord: countdown " + millisUntilFinished);
- int countdown = (int) Math.floorDiv(millisUntilFinished + 500, 1000);
+ int countdown =
+ (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(millisUntilFinished);
int resourceId = R.drawable.stat_sys_screen_record;
String description = Integer.toString(countdown);
switch (countdown) {
@@ -820,6 +844,10 @@
@Override
public void onCountdownEnd() {
+ if (Flags.statusBarScreenSharingChips()) {
+ // The screen record icon is handled in the new screen sharing chips instead of here.
+ return;
+ }
if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown");
mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
// Reset talkback priority
@@ -829,6 +857,10 @@
@Override
public void onRecordingStart() {
+ if (Flags.statusBarScreenSharingChips()) {
+ // The screen record icon is handled in the new screen sharing chips instead of here.
+ return;
+ }
if (DEBUG) Log.d(TAG, "screenrecord: showing icon");
mIconController.setIcon(mSlotScreenRecord,
R.drawable.stat_sys_screen_record,
@@ -838,6 +870,10 @@
@Override
public void onRecordingEnd() {
+ if (Flags.statusBarScreenSharingChips()) {
+ // The screen record icon is handled in the new screen sharing chips instead of here.
+ return;
+ }
// Ensure this is on the main thread
if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index d607ce0..68983a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -22,6 +22,7 @@
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.widget.ImageView
+import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.Flags
@@ -92,6 +93,8 @@
chipView.requireViewById(R.id.ongoing_activity_chip_icon)
val chipTimeView: ChipChronometer =
chipView.requireViewById(R.id.ongoing_activity_chip_time)
+ val chipTextView: TextView =
+ chipView.requireViewById(R.id.ongoing_activity_chip_text)
val chipBackgroundView =
chipView.requireViewById<ChipBackgroundContainer>(
R.id.ongoing_activity_chip_background
@@ -101,14 +104,15 @@
when (chipModel) {
is OngoingActivityChipModel.Shown -> {
// Data
- IconViewBinder.bind(chipModel.icon, chipIconView)
- ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
+ IconViewBinder.bindNullable(chipModel.icon, chipIconView)
+ setChipMainContent(chipModel, chipTextView, chipTimeView)
chipView.setOnClickListener(chipModel.onClickListener)
// Colors
val textColor = chipModel.colors.text(chipContext)
chipIconView.imageTintList = ColorStateList.valueOf(textColor)
chipTimeView.setTextColor(textColor)
+ chipTextView.setTextColor(textColor)
(chipBackgroundView.background as GradientDrawable).color =
chipModel.colors.background(chipContext)
@@ -117,6 +121,8 @@
)
}
is OngoingActivityChipModel.Hidden -> {
+ // The Chronometer should be stopped to prevent leaks -- see
+ // b/192243808 and [Chronometer.start].
chipTimeView.stop()
listener.onOngoingActivityStatusChanged(
hasOngoingActivity = false
@@ -130,6 +136,61 @@
}
}
+ private fun setChipMainContent(
+ chipModel: OngoingActivityChipModel.Shown,
+ chipTextView: TextView,
+ chipTimeView: ChipChronometer,
+ ) {
+ when (chipModel) {
+ is OngoingActivityChipModel.Shown.Countdown -> {
+ chipTextView.text = chipModel.secondsUntilStarted.toString()
+ chipTextView.visibility = View.VISIBLE
+
+ // The Chronometer should be stopped to prevent leaks -- see b/192243808 and
+ // [Chronometer.start].
+ chipTimeView.stop()
+ chipTimeView.visibility = View.GONE
+ }
+ is OngoingActivityChipModel.Shown.Timer -> {
+ ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
+ chipTimeView.visibility = View.VISIBLE
+
+ chipTextView.visibility = View.GONE
+ }
+ }
+ updateChipTextPadding(chipModel, chipTextView, chipTimeView)
+ }
+
+ private fun updateChipTextPadding(
+ chipModel: OngoingActivityChipModel.Shown,
+ chipTextView: TextView,
+ chipTimeView: ChipChronometer,
+ ) {
+ val requiresPadding = chipModel.icon != null
+ if (requiresPadding) {
+ chipTextView.addChipTextPaddingStart()
+ chipTimeView.addChipTextPaddingStart()
+ } else {
+ chipTextView.removeChipTextPaddingStart()
+ chipTimeView.removeChipTextPaddingStart()
+ }
+ }
+
+ private fun View.addChipTextPaddingStart() {
+ this.setPaddingRelative(
+ this.context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_activity_chip_icon_text_padding
+ ),
+ paddingTop,
+ paddingEnd,
+ paddingBottom,
+ )
+ }
+
+ private fun View.removeChipTextPaddingStart() {
+ this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
+ }
+
private fun animateLightsOutView(view: View, visible: Boolean) {
view.animate().cancel()
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
index 9faa84e..c5e98a1 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt
@@ -91,7 +91,7 @@
controllerFactory.create(
displaySelector = { minByOrNull { it.naturalWidth } },
effectFactory = { LinearSideLightRevealEffect(it.isVerticalRotation()) },
- overlayContainerName = SURFACE_CONTAINER_NAME
+ overlayContainerName = OVERLAY_TITLE
)
controller.init()
@@ -196,7 +196,7 @@
private companion object {
const val TAG = "FoldLightRevealOverlayAnimation"
const val WAIT_FOR_ANIMATION_TIMEOUT_MS = 2000L
- const val SURFACE_CONTAINER_NAME = "fold-overlay-container"
+ const val OVERLAY_TITLE = "fold-animation-overlay"
val ANIMATION_DURATION: Long
get() = SystemProperties.getLong("persist.fold_animation_duration", 200L)
}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
index f368cac..a921377 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/FullscreenLightRevealAnimation.kt
@@ -73,7 +73,7 @@
@Main private val executor: Executor,
@Assisted private val displaySelector: List<DisplayInfo>.() -> DisplayInfo?,
@Assisted private val lightRevealEffectFactory: (rotation: Int) -> LightRevealEffect,
- @Assisted private val overlayContainerName: String
+ @Assisted private val overlayTitle: String
) {
private lateinit var bgExecutor: Executor
@@ -156,7 +156,7 @@
val containerBuilder =
SurfaceControl.Builder(SurfaceSession())
.setContainerLayer()
- .setName(overlayContainerName)
+ .setName("FoldUnfoldAnimationContainer")
displayAreaHelper
.get()
@@ -224,7 +224,7 @@
}
format = PixelFormat.TRANSLUCENT
type = WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
- title = javaClass.simpleName
+ title = overlayTitle
layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
fitInsetsTypes = 0
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index f355dd8..666e75f 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -71,7 +71,7 @@
fullscreenLightRevealAnimationControllerFactory.create(
displaySelector = { maxByOrNull { it.naturalWidth } },
effectFactory = { LinearLightRevealEffect(it.isVerticalRotation()) },
- overlayContainerName = SURFACE_CONTAINER_NAME,
+ overlayContainerName = OVERLAY_TITLE,
)
controller.init()
bgExecutor = threadFactory.buildDelayableExecutorOnHandler(unfoldProgressHandler)
@@ -194,7 +194,7 @@
private companion object {
const val TAG = "UnfoldLightRevealOverlayAnimation"
- const val SURFACE_CONTAINER_NAME = "unfold-overlay-container"
+ const val OVERLAY_TITLE = "unfold-animation-overlay"
const val UNFOLD_BLOCK_TOUCHES_UNTIL_PROGRESS = 0.8f
}
}
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 d32d12c..a4936e6 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
@@ -37,7 +37,7 @@
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.motion.createSysUiComposeMotionTestRule
-import com.android.systemui.scene.domain.interactor.sceneContainerStartable
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.testKosmos
import org.junit.Before
import org.junit.Rule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepositoryTest.kt
new file mode 100644
index 0000000..c7e83b2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperRepositoryTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.keyboard.shortcut.data.repository
+
+import android.hardware.input.fakeInputManager
+import android.view.KeyCharacterMap.VIRTUAL_KEYBOARD
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState
+import com.android.systemui.keyboard.shortcut.shortcutHelperRepository
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutHelperRepositoryTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private val repo = kosmos.shortcutHelperRepository
+ private val helper = kosmos.shortcutHelperTestHelper
+ private val testScope = kosmos.testScope
+ private val fakeInputManager = kosmos.fakeInputManager
+
+ @Test
+ fun state_activeThroughToggle_emitsActiveWithDeviceIdFromEvent() =
+ testScope.runTest {
+ val deviceId = 123
+ val state by collectLastValue(repo.state)
+
+ helper.toggle(deviceId)
+
+ assertThat(state).isEqualTo(ShortcutHelperState.Active(deviceId))
+ }
+
+ @Test
+ fun state_activeThroughActivity_noKeyboardActive_emitsActiveWithVirtualDeviceId() =
+ testScope.runTest {
+ val state by collectLastValue(repo.state)
+
+ helper.showFromActivity()
+
+ assertThat(state).isEqualTo(ShortcutHelperState.Active(VIRTUAL_KEYBOARD))
+ }
+
+ @Test
+ fun state_activeThroughActivity_virtualKeyboardActive_emitsActiveWithVirtualDeviceId() =
+ testScope.runTest {
+ val state by collectLastValue(repo.state)
+
+ fakeInputManager.addVirtualKeyboard()
+ helper.showFromActivity()
+
+ assertThat(state).isEqualTo(ShortcutHelperState.Active(VIRTUAL_KEYBOARD))
+ }
+
+ @Test
+ fun state_activeThroughActivity_physicalKeyboardActive_emitsActiveWithDeviceId() =
+ testScope.runTest {
+ val deviceId = 456
+ val state by collectLastValue(repo.state)
+
+ fakeInputManager.addPhysicalKeyboard(deviceId)
+ helper.showFromActivity()
+
+ assertThat(state).isEqualTo(ShortcutHelperState.Active(deviceId))
+ }
+
+ @Test
+ fun state_activeThroughActivity_physicalKeyboardDisabled_emitsActiveWithVirtualDeviceId() =
+ testScope.runTest {
+ val deviceId = 456
+ val state by collectLastValue(repo.state)
+
+ fakeInputManager.addPhysicalKeyboard(deviceId)
+ fakeInputManager.inputManager.disableInputDevice(deviceId)
+ helper.showFromActivity()
+
+ assertThat(state).isEqualTo(ShortcutHelperState.Active(VIRTUAL_KEYBOARD))
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index ecc456c..a770722 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -40,6 +40,7 @@
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.os.Bundle
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
@@ -1771,8 +1772,40 @@
verify(logger).logSeek(anyInt(), eq(PACKAGE), eq(instanceId))
}
+ @EnableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
@Test
- fun tapContentView_showOverLockscreen_openActivity() {
+ fun tapContentView_showOverLockscreen_openActivity_withOriginAnimation() {
+ // WHEN we are on lockscreen and this activity can show over lockscreen
+ whenever(keyguardStateController.isShowing).thenReturn(true)
+ whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true)
+
+ val clickIntent = mock(Intent::class.java)
+ val pendingIntent = mock(PendingIntent::class.java)
+ whenever(pendingIntent.intent).thenReturn(clickIntent)
+ val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+ val data = mediaData.copy(clickIntent = pendingIntent)
+ player.attachPlayer(viewHolder)
+ player.bindPlayer(data, KEY)
+ verify(viewHolder.player).setOnClickListener(captor.capture())
+
+ // THEN it sends the PendingIntent without dismissing keyguard first,
+ // and does not use the Intent directly (see b/271845008)
+ captor.value.onClick(viewHolder.player)
+ verify(activityStarter)
+ .startPendingIntentMaybeDismissingKeyguard(
+ eq(pendingIntent),
+ eq(true),
+ eq(null),
+ any(),
+ eq(null),
+ eq(null),
+ )
+ verify(activityStarter, never()).postStartActivityDismissingKeyguard(eq(clickIntent), any())
+ }
+
+ @DisableFlags(Flags.FLAG_MEDIA_LOCKSCREEN_LAUNCH_ANIMATION)
+ @Test
+ fun tapContentView_showOverLockscreen_openActivity_withoutOriginAnimation() {
// WHEN we are on lockscreen and this activity can show over lockscreen
whenever(keyguardStateController.isShowing).thenReturn(true)
whenever(activityIntentHelper.wouldPendingShowOverLockscreen(any(), any())).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
index 2ff660f..bfbb7ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavBarHelperTest.java
@@ -36,6 +36,7 @@
import android.content.ComponentName;
import android.content.res.Configuration;
+import android.os.Handler;
import android.view.IWindowManager;
import android.view.accessibility.AccessibilityManager;
@@ -65,6 +66,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -120,6 +123,10 @@
EdgeBackGestureHandler.Factory mEdgeBackGestureHandlerFactory;
@Mock
NotificationShadeWindowController mNotificationShadeWindowController;
+ @Mock
+ Handler mBgHandler;
+
+ @Captor ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
ConfigurationController mConfigurationController = new FakeConfigurationController();
private AccessibilityManager.AccessibilityServicesStateChangeListener
@@ -149,7 +156,7 @@
() -> Optional.of(mock(CentralSurfaces.class)), mock(KeyguardStateController.class),
mNavigationModeController, mEdgeBackGestureHandlerFactory, mWm, mUserTracker,
mDisplayTracker, mNotificationShadeWindowController, mConfigurationController,
- mDumpManager, mCommandQueue, mSynchronousExecutor);
+ mDumpManager, mCommandQueue, mSynchronousExecutor, mBgHandler);
}
@Test
@@ -203,8 +210,10 @@
.updateAccessibilityServicesState();
verify(mNavbarTaskbarStateUpdater, times(1))
.updateAssistantAvailable(anyBoolean(), anyBoolean());
+ verify(mBgHandler).post(mRunnableArgumentCaptor.capture());
+ mRunnableArgumentCaptor.getValue().run();
verify(mNavbarTaskbarStateUpdater, times(1))
- .updateRotationWatcherState(anyInt());
+ .updateRotationWatcherState(anyInt(), anyBoolean());
verify(mNavbarTaskbarStateUpdater, times(1))
.updateWallpaperVisibility(anyBoolean(), anyInt());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
index d5361ac..df8eafe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerImplTest.java
@@ -21,6 +21,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.wm.shell.Flags.enableTaskbarNavbarUnification;
+import static com.android.wm.shell.Flags.enableTaskbarOnPhones;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -142,10 +143,11 @@
@Test
public void testCreateNavigationBarsIncludeDefaultTrue() {
- assumeFalse(enableTaskbarNavbarUnification());
+ assumeFalse(enableTaskbarNavbarUnification() && enableTaskbarOnPhones());
// Large screens may be using taskbar and the logic is different
mNavigationBarController.mIsLargeScreen = false;
+ mNavigationBarController.mIsPhone = true;
doNothing().when(mNavigationBarController).createNavigationBar(any(), any(), any());
mNavigationBarController.createNavigationBars(true, null);
@@ -291,6 +293,17 @@
@Test
public void testShouldRenderTaskbar_taskbarNotRenderedOnPhone() {
+ assumeFalse(enableTaskbarOnPhones());
+
+ mNavigationBarController.mIsLargeScreen = false;
+ mNavigationBarController.mIsPhone = true;
+ assertFalse(mNavigationBarController.supportsTaskbar());
+ }
+
+ @Test
+ public void testShouldRenderTaskbar_taskbarRenderedOnPhone() {
+ assumeTrue(enableTaskbarNavbarUnification() && enableTaskbarOnPhones());
+
mNavigationBarController.mIsLargeScreen = false;
mNavigationBarController.mIsPhone = true;
assertFalse(mNavigationBarController.supportsTaskbar());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 2b60f65..982a269 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -179,6 +179,9 @@
private SysUiState mMockSysUiState;
@Mock
private Handler mHandler;
+
+ @Mock
+ private Handler mBgHandler;
@Mock
private UserTracker mUserTracker;
@Mock
@@ -277,7 +280,8 @@
mEdgeBackGestureHandlerFactory, mock(IWindowManager.class),
mock(UserTracker.class), mock(DisplayTracker.class),
mNotificationShadeWindowController, mock(ConfigurationController.class),
- mock(DumpManager.class), mock(CommandQueue.class), mSynchronousExecutor));
+ mock(DumpManager.class), mock(CommandQueue.class), mSynchronousExecutor,
+ mBgHandler));
mNavigationBar = createNavBar(mContext);
mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
});
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
index 1c86638..03483c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
@@ -20,6 +20,8 @@
import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@@ -279,4 +281,23 @@
verify(mTileLifecycle, never()).onStopListening();
verify(mTileLifecycle, never()).executeSetBindService(false);
}
+
+ @Test
+ public void testNoExtraPendingBindIfAlreadyBound() {
+ mTileServiceManager.startLifecycleManagerAndAddTile();
+
+ // As part of adding the tile, it will be bound and it will send a start successful to
+ // TileServices. startSuccessful will clear pending bind
+ mTileServiceManager.clearPendingBind();
+
+ // Assume we are still bound
+ when(mTileLifecycle.isBound()).thenReturn(true);
+
+ // And we want to bind again
+ mTileServiceManager.setBindAllowed(true);
+ mTileServiceManager.setBindRequested(true);
+
+ // Then the tile doesn't have pending bind
+ assertThat(mTileServiceManager.hasPendingBind()).isFalse();
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 503c52f..ce1a885 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -17,7 +17,6 @@
package com.android.systemui.recordissue
import android.app.Dialog
-import android.content.SharedPreferences
import android.os.UserHandle
import android.testing.TestableLooper
import android.widget.Button
@@ -57,6 +56,7 @@
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -71,7 +71,6 @@
@Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var state: IssueRecordingState
- @Mock private lateinit var sharedPreferences: SharedPreferences
@Mock
private lateinit var screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate
@Mock private lateinit var screenCaptureDisabledDialog: SystemUIDialog
@@ -192,7 +191,7 @@
anyInt(),
eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
)
- verify(factory).create(any<ScreenCapturePermissionDialogDelegate>())
+ verify(factory, times(2)).create(any(SystemUIDialog.Delegate::class.java))
}
@Test
@@ -213,7 +212,7 @@
anyInt(),
eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
)
- verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
+ verify(factory, never()).create()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
new file mode 100644
index 0000000..9331c8d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.screenrecord.data.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+
+@SmallTest
+class ScreenRecordModelTest : SysuiTestCase() {
+ @Test
+ fun countdownSeconds_millis0_is0() {
+ assertThat(0L.toCountdownSeconds()).isEqualTo(0)
+ assertThat(ScreenRecordModel.Starting(0L).countdownSeconds).isEqualTo(0)
+ }
+
+ @Test
+ fun countdownSeconds_millis500_isOne() {
+ assertThat(500L.toCountdownSeconds()).isEqualTo(1)
+ assertThat(ScreenRecordModel.Starting(500L).countdownSeconds).isEqualTo(1)
+ }
+
+ @Test
+ fun countdownSeconds_millis999_isOne() {
+ assertThat(999L.toCountdownSeconds()).isEqualTo(1)
+ assertThat(ScreenRecordModel.Starting(999L).countdownSeconds).isEqualTo(1)
+ }
+
+ @Test
+ fun countdownSeconds_millis1000_isOne() {
+ assertThat(1000L.toCountdownSeconds()).isEqualTo(1)
+ assertThat(ScreenRecordModel.Starting(1000L).countdownSeconds).isEqualTo(1)
+ }
+
+ @Test
+ fun countdownSeconds_millis1499_isOne() {
+ assertThat(1499L.toCountdownSeconds()).isEqualTo(1)
+ assertThat(ScreenRecordModel.Starting(1499L).countdownSeconds).isEqualTo(1)
+ }
+
+ @Test
+ fun countdownSeconds_millis1500_isTwo() {
+ assertThat(1500L.toCountdownSeconds()).isEqualTo(2)
+ assertThat(ScreenRecordModel.Starting(1500L).countdownSeconds).isEqualTo(2)
+ }
+
+ @Test
+ fun countdownSeconds_millis1999_isTwo() {
+ assertThat(1599L.toCountdownSeconds()).isEqualTo(2)
+ assertThat(ScreenRecordModel.Starting(1599L).countdownSeconds).isEqualTo(2)
+ }
+
+ @Test
+ fun countdownSeconds_millis2000_isTwo() {
+ assertThat(2000L.toCountdownSeconds()).isEqualTo(2)
+ assertThat(ScreenRecordModel.Starting(2000L).countdownSeconds).isEqualTo(2)
+ }
+
+ @Test
+ fun countdownSeconds_millis2500_isThree() {
+ assertThat(2500L.toCountdownSeconds()).isEqualTo(3)
+ assertThat(ScreenRecordModel.Starting(2500L).countdownSeconds).isEqualTo(3)
+ }
+
+ @Test
+ fun countdownSeconds_millis2999_isThree() {
+ assertThat(2999L.toCountdownSeconds()).isEqualTo(3)
+ assertThat(ScreenRecordModel.Starting(2999L).countdownSeconds).isEqualTo(3)
+ }
+
+ @Test
+ fun countdownSeconds_millis3000_isThree() {
+ assertThat(3000L.toCountdownSeconds()).isEqualTo(3)
+ assertThat(ScreenRecordModel.Starting(3000L).countdownSeconds).isEqualTo(3)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index ec5589e..0b81b5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -3,9 +3,6 @@
import android.content.ComponentName
import android.graphics.Bitmap
import android.net.Uri
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.Display.TYPE_INTERNAL
@@ -18,7 +15,6 @@
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.util.ScreenshotRequest
-import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.display.data.repository.display
@@ -26,7 +22,6 @@
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.kotlinArgumentCaptor as ArgumentCaptor
import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.lang.IllegalStateException
@@ -47,8 +42,7 @@
@SmallTest
class TakeScreenshotExecutorTest : SysuiTestCase() {
- private val controller0 = mock<ScreenshotController>()
- private val controller1 = mock<ScreenshotController>()
+ private val controller = mock<ScreenshotController>()
private val notificationsController0 = mock<ScreenshotNotificationsController>()
private val notificationsController1 = mock<ScreenshotNotificationsController>()
private val controllerFactory = mock<ScreenshotController.Factory>()
@@ -60,6 +54,7 @@
private val topComponent = ComponentName(mContext, TakeScreenshotExecutorTest::class.java)
private val testScope = TestScope(UnconfinedTestDispatcher())
private val eventLogger = UiEventLoggerFake()
+ private val headlessHandler = mock<HeadlessScreenshotHandler>()
private val screenshotExecutor =
TakeScreenshotExecutorImpl(
@@ -68,20 +63,18 @@
testScope,
requestProcessor,
eventLogger,
- notificationControllerFactory
+ notificationControllerFactory,
+ headlessHandler,
)
@Before
fun setUp() {
- whenever(controllerFactory.create(any(), any())).thenAnswer {
- if (it.getArgument<Display>(0).displayId == 0) controller0 else controller1
- }
+ whenever(controllerFactory.create(any(), any())).thenReturn(controller)
whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0)
whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1)
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
testScope.runTest {
val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -91,14 +84,14 @@
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
verify(controllerFactory).create(eq(internalDisplay), any())
- verify(controllerFactory).create(eq(externalDisplay), any())
+ verify(controllerFactory, never()).create(eq(externalDisplay), any())
val capturer = ArgumentCaptor<ScreenshotData>()
- verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+ verify(controller).handleScreenshot(capturer.capture(), any(), any())
assertThat(capturer.value.displayId).isEqualTo(0)
// OnSaved callback should be different.
- verify(controller1).handleScreenshot(capturer.capture(), any(), any())
+ verify(headlessHandler).handleScreenshot(capturer.capture(), any(), any())
assertThat(capturer.value.displayId).isEqualTo(1)
assertThat(eventLogger.numLogs()).isEqualTo(2)
@@ -113,32 +106,6 @@
}
@Test
- @EnableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
- fun executeScreenshots_severalDisplaysShelfUi_justCallsOne() =
- testScope.runTest {
- val internalDisplay = display(TYPE_INTERNAL, id = 0)
- val externalDisplay = display(TYPE_EXTERNAL, id = 1)
- setDisplays(internalDisplay, externalDisplay)
- val onSaved = { _: Uri? -> }
- screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
-
- verify(controllerFactory).create(eq(internalDisplay), any())
-
- val capturer = ArgumentCaptor<ScreenshotData>()
-
- verify(controller0).handleScreenshot(capturer.capture(), any(), any())
- assertThat(capturer.value.displayId).isEqualTo(0)
-
- assertThat(eventLogger.numLogs()).isEqualTo(1)
- assertThat(eventLogger.get(0).eventId)
- .isEqualTo(ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER.id)
- assertThat(eventLogger.get(0).packageName).isEqualTo(topComponent.packageName)
-
- screenshotExecutor.onDestroy()
- }
-
- @Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
testScope.runTest {
val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -156,10 +123,10 @@
val capturer = ArgumentCaptor<ScreenshotData>()
- verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+ verify(controller).handleScreenshot(capturer.capture(), any(), any())
assertThat(capturer.value.displayId).isEqualTo(0)
// OnSaved callback should be different.
- verify(controller1, never()).handleScreenshot(any(), any(), any())
+ verify(headlessHandler, never()).handleScreenshot(any(), any(), any())
assertThat(eventLogger.numLogs()).isEqualTo(1)
assertThat(eventLogger.get(0).eventId)
@@ -170,7 +137,6 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_onlyVirtualDisplays_noInteractionsWithControllers() =
testScope.runTest {
setDisplays(display(TYPE_VIRTUAL, id = 0), display(TYPE_VIRTUAL, id = 1))
@@ -178,14 +144,14 @@
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
verifyNoMoreInteractions(controllerFactory)
+ verify(headlessHandler, never()).handleScreenshot(any(), any(), any())
screenshotExecutor.onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_allowedTypes_allCaptured() =
testScope.runTest {
- whenever(controllerFactory.create(any(), any())).thenReturn(controller0)
+ whenever(controllerFactory.create(any(), any())).thenReturn(controller)
setDisplays(
display(TYPE_INTERNAL, id = 0),
@@ -196,12 +162,12 @@
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
- verify(controller0, times(4)).handleScreenshot(any(), any(), any())
+ verify(controller, times(1)).handleScreenshot(any(), any(), any())
+ verify(headlessHandler, times(3)).handleScreenshot(any(), any(), any())
screenshotExecutor.onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_reportsOnFinishedOnlyWhenBothFinished() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -211,8 +177,8 @@
val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
- verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
- verify(controller1).handleScreenshot(any(), any(), capturer1.capture())
+ verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+ verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
verify(callback, never()).onFinish()
@@ -227,7 +193,6 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_oneFinishesOtherFails_reportFailsOnlyAtTheEnd() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -237,8 +202,8 @@
val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
- verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
- verify(controller1).handleScreenshot(any(), nullable(), capturer1.capture())
+ verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+ verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
verify(callback, never()).onFinish()
@@ -255,7 +220,6 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_allDisplaysFail_reportsFail() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -265,8 +229,8 @@
val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
- verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
- verify(controller1).handleScreenshot(any(), any(), capturer1.capture())
+ verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+ verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
verify(callback, never()).onFinish()
@@ -283,7 +247,6 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun onDestroy_propagatedToControllers() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -291,59 +254,50 @@
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
screenshotExecutor.onDestroy()
- verify(controller0).onDestroy()
- verify(controller1).onDestroy()
+ verify(controller).onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
- fun removeWindows_propagatedToControllers() =
+ fun removeWindows_propagatedToController() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
screenshotExecutor.removeWindows()
- verify(controller0).removeWindow()
- verify(controller1).removeWindow()
+ verify(controller).removeWindow()
screenshotExecutor.onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
- fun onCloseSystemDialogsReceived_propagatedToControllers() =
+ fun onCloseSystemDialogsReceived_propagatedToController() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
screenshotExecutor.onCloseSystemDialogsReceived()
- verify(controller0).requestDismissal(any())
- verify(controller1).requestDismissal(any())
+ verify(controller).requestDismissal(any())
screenshotExecutor.onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
- fun onCloseSystemDialogsReceived_someControllerHavePendingTransitions() =
+ fun onCloseSystemDialogsReceived_controllerHasPendingTransitions() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
- whenever(controller0.isPendingSharedTransition).thenReturn(true)
- whenever(controller1.isPendingSharedTransition).thenReturn(false)
+ whenever(controller.isPendingSharedTransition).thenReturn(true)
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
screenshotExecutor.onCloseSystemDialogsReceived()
- verify(controller0, never()).requestDismissal(any())
- verify(controller1).requestDismissal(any())
+ verify(controller, never()).requestDismissal(any())
screenshotExecutor.onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_controllerCalledWithRequestProcessorReturnValue() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0))
@@ -358,14 +312,13 @@
.isEqualTo(ScreenshotData.fromRequest(screenshotRequest))
val capturer = ArgumentCaptor<ScreenshotData>()
- verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+ verify(controller).handleScreenshot(capturer.capture(), any(), any())
assertThat(capturer.value).isEqualTo(toBeReturnedByProcessor)
screenshotExecutor.onDestroy()
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_errorFromProcessor_logsScreenshotRequested() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -383,7 +336,6 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_errorFromProcessor_logsUiError() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -401,7 +353,6 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_errorFromProcessorOnDefaultDisplay_showsErrorNotification() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -428,14 +379,13 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
- whenever(controller0.handleScreenshot(any(), any(), any()))
+ whenever(controller.handleScreenshot(any(), any(), any()))
.thenThrow(IllegalStateException::class.java)
- whenever(controller1.handleScreenshot(any(), any(), any()))
+ whenever(headlessHandler.handleScreenshot(any(), any(), any()))
.thenThrow(IllegalStateException::class.java)
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -449,14 +399,13 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_errorFromScreenshotController_reportsError() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
- whenever(controller0.handleScreenshot(any(), any(), any()))
+ whenever(controller.handleScreenshot(any(), any(), any()))
.thenThrow(IllegalStateException::class.java)
- whenever(controller1.handleScreenshot(any(), any(), any()))
+ whenever(headlessHandler.handleScreenshot(any(), any(), any()))
.thenThrow(IllegalStateException::class.java)
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -470,14 +419,13 @@
}
@Test
- @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
- whenever(controller0.handleScreenshot(any(), any(), any()))
+ whenever(controller.handleScreenshot(any(), any(), any()))
.thenThrow(IllegalStateException::class.java)
- whenever(controller1.handleScreenshot(any(), any(), any()))
+ whenever(headlessHandler.handleScreenshot(any(), any(), any()))
.thenThrow(IllegalStateException::class.java)
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -496,7 +444,7 @@
assertThat(it).isNull()
onSavedCallCount += 1
}
- whenever(controller0.handleScreenshot(any(), any(), any())).thenAnswer {
+ whenever(controller.handleScreenshot(any(), any(), any())).thenAnswer {
(it.getArgument(1) as Consumer<Uri?>).accept(null)
}
@@ -525,6 +473,7 @@
var processed: ScreenshotData? = null
var toReturn: ScreenshotData? = null
var shouldThrowException = false
+
override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
if (shouldThrowException) throw RequestProcessorException("")
processed = screenshot
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 3606b1b..c3e810e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -69,13 +69,13 @@
}
@Test
- fun chip_inCall_isShown() =
+ fun chip_inCall_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 345, intent = null))
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
}
@Test
@@ -92,7 +92,8 @@
// started 2000ms ago (1000 - 3000). The OngoingActivityChipModel start time needs to be
// relative to elapsedRealtime, so it should be 2000ms before the elapsed realtime set
// on the clock.
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+ .isEqualTo(398_000)
}
@Test
@@ -127,7 +128,8 @@
// Start a call
repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+ .isEqualTo(398_000)
// End the call
repo.setOngoingCallState(OngoingCallModel.NoCall)
@@ -140,20 +142,18 @@
// Start a new call, which started 1000ms ago
repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 102_000, intent = null))
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(499_000)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+ .isEqualTo(499_000)
}
@Test
- fun chip_inCall_nullIntent_clickListenerDoesNothing() =
+ fun chip_inCall_nullIntent_nullClickListener() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
- val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
-
- clickListener.onClick(chipView)
- // Just verify nothing crashes
+ assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
}
@Test
@@ -164,8 +164,9 @@
val intent = mock<PendingIntent>()
repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = intent))
val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index d7935e5..bde668e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -91,7 +91,7 @@
}
@Test
- fun chip_singleTaskState_otherDevicesPackage_isShown() =
+ fun chip_singleTaskState_otherDevicesPackage_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -101,20 +101,20 @@
createTask(taskId = 1),
)
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
}
@Test
- fun chip_entireScreenState_otherDevicesPackage_isShown() =
+ fun chip_entireScreenState_otherDevicesPackage_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
}
@@ -162,7 +162,7 @@
MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -175,7 +175,7 @@
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
}
@Test
@@ -186,8 +186,9 @@
MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
eq(mockCastDialog),
@@ -209,8 +210,9 @@
)
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
eq(mockCastDialog),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index fdf0e5d..8e8b082 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -88,29 +88,75 @@
}
@Test
- fun chip_startingState_isHidden() =
+ fun chip_startingState_isShownAsCountdownWithoutIconOrClickListener() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(400)
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Countdown::class.java)
+ assertThat((latest as OngoingActivityChipModel.Shown).icon).isNull()
+ assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
+ }
+
+ // The millis we typically get from [ScreenRecordRepository] are around 2995, 1995, and 995.
+ @Test
+ fun chip_startingState_millis2995_is3() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2995)
+
+ assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+ .isEqualTo(3)
}
@Test
- fun chip_recordingState_isShownWithIcon() =
+ fun chip_startingState_millis1995_is2() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(1995)
+
+ assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+ .isEqualTo(2)
+ }
+
+ @Test
+ fun chip_startingState_millis995_is1() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(995)
+
+ assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+ .isEqualTo(1)
+ }
+
+ @Test
+ fun chip_recordingState_isShownAsTimerWithIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
}
@Test
- fun chip_colorsAreRed() =
+ fun chip_startingState_colorsAreRed() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2000L)
+
+ assertThat((latest as OngoingActivityChipModel.Shown).colors).isEqualTo(ColorsModel.Red)
+ }
+
+ @Test
+ fun chip_recordingState_colorsAreRed() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -128,7 +174,7 @@
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -137,7 +183,7 @@
screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
}
@Test
@@ -148,8 +194,9 @@
mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
// EndScreenRecordingDialogDelegate will test that the dialog has the right message
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
@@ -169,8 +216,9 @@
MediaProjectionState.Projecting.EntireScreen("host.package")
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
// EndScreenRecordingDialogDelegate will test that the dialog has the right message
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
@@ -193,8 +241,9 @@
)
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
// EndScreenRecordingDialogDelegate will test that the dialog has the right message
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
index 4c2546e..63c29ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -59,7 +59,7 @@
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
- verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
+ verify(sysuiDialog).setIcon(R.drawable.ic_present_to_all)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 8ea3f4a..2e5f7f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -116,7 +116,7 @@
}
@Test
- fun chip_singleTaskState_normalPackage_isShown() =
+ fun chip_singleTaskState_normalPackage_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -126,22 +126,22 @@
createTask(taskId = 1),
)
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
- assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
}
@Test
- fun chip_entireScreenState_normalPackage_isShown() =
+ fun chip_entireScreenState_normalPackage_isShownAsTimer() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
mediaProjectionRepo.mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
- assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
}
@Test
@@ -165,7 +165,7 @@
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -178,7 +178,7 @@
)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
}
@Test
@@ -189,8 +189,9 @@
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
eq(mockShareDialog),
@@ -211,8 +212,9 @@
)
val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+ assertThat(clickListener).isNotNull()
- clickListener.onClick(chipView)
+ clickListener!!.onClick(chipView)
verify(kosmos.mockDialogTransitionAnimator)
.showFromView(
eq(mockShareDialog),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 912a10a..8bc83cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -35,7 +35,11 @@
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -46,6 +50,7 @@
class OngoingActivityChipsViewModelTest : SysuiTestCase() {
private val kosmos = Kosmos().also { it.testCase = this }
private val testScope = kosmos.testScope
+ private val systemClock = kosmos.fakeSystemClock
private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
@@ -191,6 +196,39 @@
assertIsCallChip(latest)
}
+ /** Regression test for b/347726238. */
+ @Test
+ fun chip_timerDoesNotResetAfterSubscribersRestart() =
+ testScope.runTest {
+ var latest: OngoingActivityChipModel? = null
+
+ val job1 = underTest.chip.onEach { latest = it }.launchIn(this)
+
+ // Start a chip with a timer
+ systemClock.setElapsedRealtime(1234)
+ screenRecordState.value = ScreenRecordModel.Recording
+
+ runCurrent()
+
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+ // Stop subscribing to the chip flow
+ job1.cancel()
+
+ // Let time pass
+ systemClock.setElapsedRealtime(5678)
+
+ // WHEN we re-subscribe to the chip flow
+ val job2 = underTest.chip.onEach { latest = it }.launchIn(this)
+
+ runCurrent()
+
+ // THEN the old start time is still used
+ assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+ job2.cancel()
+ }
+
companion object {
fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
@@ -201,7 +239,7 @@
fun assertIsShareToAppChip(latest: OngoingActivityChipModel?) {
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
val icon = (latest as OngoingActivityChipModel.Shown).icon
- assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+ assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
}
fun assertIsCallChip(latest: OngoingActivityChipModel?) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 0766afc..b8f8026 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -328,7 +328,7 @@
@Test
@EnableFlags(NotificationContentAlphaOptimization.FLAG_NAME)
- public void setHideSensitive_changeContent_shouldNotDisturbAnimation() throws Exception {
+ public void setHideSensitive_changeContent_shouldResetAlpha() throws Exception {
// Given: A sensitive row that has public version but is not hiding sensitive,
// and is during an animation that sets its alpha value to be 0.5f
@@ -351,12 +351,12 @@
// Then: The alpha value of private layout should be reset to 1, private layout be
// INVISIBLE;
- // The alpha value of public layout should be 0.5 to preserve the animation state, public
- // layout should be VISIBLE
+ // The alpha value of public layout should be reset to 1 to avoid remaining transparent,
+ // public layout should be VISIBLE
assertEquals(View.INVISIBLE, row.getPrivateLayout().getVisibility());
assertEquals(1f, row.getPrivateLayout().getAlpha(), 0);
assertEquals(View.VISIBLE, row.getPublicLayout().getVisibility());
- assertEquals(0.5f, row.getPublicLayout().getAlpha(), 0);
+ assertEquals(1f, row.getPublicLayout().getAlpha(), 0);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index e6cba1c..54a26f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -23,6 +23,7 @@
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper.RunWithLooper
import android.util.TypedValue
+import android.util.TypedValue.COMPLEX_UNIT_SP
import android.view.View
import android.view.ViewGroup
import android.widget.RemoteViews
@@ -34,27 +35,39 @@
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
import com.android.systemui.statusbar.policy.InflatedSmartReplyState
import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder
import com.android.systemui.statusbar.policy.SmartReplyStateInflater
-import com.android.systemui.util.concurrency.mockExecutorHandler
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.DisposableHandle
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
+import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.spy
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
@@ -65,20 +78,24 @@
@RunWithLooper
@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME)
class NotificationRowContentBinderImplTest : SysuiTestCase() {
- private lateinit var mNotificationInflater: NotificationRowContentBinderImpl
- private lateinit var mBuilder: Notification.Builder
- private lateinit var mRow: ExpandableNotificationRow
- private lateinit var mHelper: NotificationTestHelper
+ private lateinit var notificationInflater: NotificationRowContentBinderImpl
+ private lateinit var builder: Notification.Builder
+ private lateinit var row: ExpandableNotificationRow
+ private lateinit var testHelper: NotificationTestHelper
- private var mCache: NotifRemoteViewCache = mock()
- private var mConversationNotificationProcessor: ConversationNotificationProcessor = mock()
- private var mInflatedSmartReplyState: InflatedSmartReplyState = mock()
- private var mInflatedSmartReplies: InflatedSmartReplyViewHolder = mock()
- private var mNotifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider = mock()
- private var mHeadsUpStyleProvider: HeadsUpStyleProvider = mock()
- private var mNotifLayoutInflaterFactory: NotifLayoutInflaterFactory = mock()
- private val mSmartReplyStateInflater: SmartReplyStateInflater =
+ private val cache: NotifRemoteViewCache = mock()
+ private val layoutInflaterFactoryProvider =
+ object : NotifLayoutInflaterFactory.Provider {
+ override fun provide(
+ row: ExpandableNotificationRow,
+ layoutType: Int
+ ): NotifLayoutInflaterFactory = mock()
+ }
+ private val smartReplyStateInflater: SmartReplyStateInflater =
object : SmartReplyStateInflater {
+ private val inflatedSmartReplyState: InflatedSmartReplyState = mock()
+ private val inflatedSmartReplies: InflatedSmartReplyViewHolder = mock()
+
override fun inflateSmartReplyViewHolder(
sysuiContext: Context,
notifPackageContext: Context,
@@ -86,37 +103,61 @@
existingSmartReplyState: InflatedSmartReplyState?,
newSmartReplyState: InflatedSmartReplyState
): InflatedSmartReplyViewHolder {
- return mInflatedSmartReplies
+ return inflatedSmartReplies
}
override fun inflateSmartReplyState(entry: NotificationEntry): InflatedSmartReplyState {
- return mInflatedSmartReplyState
+ return inflatedSmartReplyState
}
}
+ private var fakeRonContentModel: RichOngoingContentModel? = null
+ private val fakeRonExtractor =
+ object : RichOngoingNotificationContentExtractor {
+ override fun extractContentModel(
+ entry: NotificationEntry,
+ builder: Notification.Builder,
+ systemUIContext: Context,
+ packageContext: Context
+ ): RichOngoingContentModel? = fakeRonContentModel
+ }
+
+ private var fakeRonViewHolder: InflatedContentViewHolder? = null
+ private val fakeRonViewInflater =
+ spy(
+ object : RichOngoingNotificationViewInflater {
+ override fun inflateView(
+ contentModel: RichOngoingContentModel,
+ existingView: View?,
+ entry: NotificationEntry,
+ systemUiContext: Context,
+ parentView: ViewGroup
+ ): InflatedContentViewHolder? = fakeRonViewHolder
+ }
+ )
+
@Before
fun setUp() {
allowTestableLooperAsMainThread()
- mBuilder =
+ builder =
Notification.Builder(mContext, "no-id")
.setSmallIcon(R.drawable.ic_person)
.setContentTitle("Title")
.setContentText("Text")
.setStyle(Notification.BigTextStyle().bigText("big text"))
- mHelper = NotificationTestHelper(mContext, mDependency)
- val row = mHelper.createRow(mBuilder.build())
- mRow = spy(row)
- whenever(mNotifLayoutInflaterFactoryProvider.provide(any(), any()))
- .thenReturn(mNotifLayoutInflaterFactory)
- mNotificationInflater =
+ testHelper = NotificationTestHelper(mContext, mDependency)
+ row = spy(testHelper.createRow(builder.build()))
+ notificationInflater =
NotificationRowContentBinderImpl(
- mCache,
+ cache,
mock(),
- mConversationNotificationProcessor,
+ mock<ConversationNotificationProcessor>(),
+ fakeRonExtractor,
+ fakeRonViewInflater,
mock(),
- mSmartReplyStateInflater,
- mNotifLayoutInflaterFactoryProvider,
- mHeadsUpStyleProvider,
+ smartReplyStateInflater,
+ layoutInflaterFactoryProvider,
+ mock<HeadsUpStyleProvider>(),
mock()
)
}
@@ -125,16 +166,16 @@
fun testIncreasedHeadsUpBeingUsed() {
val params = BindParams()
params.usesIncreasedHeadsUpHeight = true
- val builder = spy(mBuilder)
- mNotificationInflater.inflateNotificationViews(
- mRow.entry,
- mRow,
+ val builder = spy(builder)
+ notificationInflater.inflateNotificationViews(
+ row.entry,
+ row,
params,
true /* inflateSynchronously */,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
+ FLAG_CONTENT_VIEW_ALL,
builder,
mContext,
- mSmartReplyStateInflater
+ smartReplyStateInflater
)
verify(builder).createHeadsUpContentView(true)
}
@@ -143,80 +184,68 @@
fun testIncreasedHeightBeingUsed() {
val params = BindParams()
params.usesIncreasedHeight = true
- val builder = spy(mBuilder)
- mNotificationInflater.inflateNotificationViews(
- mRow.entry,
- mRow,
+ val builder = spy(builder)
+ notificationInflater.inflateNotificationViews(
+ row.entry,
+ row,
params,
true /* inflateSynchronously */,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
+ FLAG_CONTENT_VIEW_ALL,
builder,
mContext,
- mSmartReplyStateInflater
+ smartReplyStateInflater
)
verify(builder).createContentView(true)
}
@Test
fun testInflationCallsUpdated() {
- inflateAndWait(
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
- mRow
- )
- verify(mRow).onNotificationUpdated()
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
+ verify(row).onNotificationUpdated()
}
@Test
fun testInflationOnlyInflatesSetFlags() {
- inflateAndWait(
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP,
- mRow
- )
- Assert.assertNotNull(mRow.privateLayout.headsUpChild)
- verify(mRow).onNotificationUpdated()
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_HEADS_UP, row)
+ Assert.assertNotNull(row.privateLayout.headsUpChild)
+ verify(row).onNotificationUpdated()
}
@Test
fun testInflationThrowsErrorDoesntCallUpdated() {
- mRow.privateLayout.removeAllViews()
- mRow.entry.sbn.notification.contentView =
+ row.privateLayout.removeAllViews()
+ row.entry.sbn.notification.contentView =
RemoteViews(mContext.packageName, R.layout.status_bar)
inflateAndWait(
true /* expectingException */,
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
- mRow
+ notificationInflater,
+ FLAG_CONTENT_VIEW_ALL,
+ row
)
- Assert.assertTrue(mRow.privateLayout.childCount == 0)
- verify(mRow, times(0)).onNotificationUpdated()
+ Assert.assertTrue(row.privateLayout.childCount == 0)
+ verify(row, times(0)).onNotificationUpdated()
}
@Test
fun testAsyncTaskRemoved() {
- mRow.entry.abortTask()
- inflateAndWait(
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
- mRow
- )
- verify(mRow).onNotificationUpdated()
+ row.entry.abortTask()
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
+ verify(row).onNotificationUpdated()
}
@Test
fun testRemovedNotInflated() {
- mRow.setRemoved()
- mNotificationInflater.setInflateSynchronously(true)
- mNotificationInflater.bindContent(
- mRow.entry,
- mRow,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
+ row.setRemoved()
+ notificationInflater.setInflateSynchronously(true)
+ notificationInflater.bindContent(
+ row.entry,
+ row,
+ FLAG_CONTENT_VIEW_ALL,
BindParams(),
false /* forceInflate */,
null /* callback */
)
- Assert.assertNull(mRow.entry.runningTask)
+ Assert.assertNull(row.entry.runningTask)
}
@Test
@@ -235,11 +264,11 @@
inflateSynchronously = false,
isMinimized = false,
result = result,
- reInflateFlags = NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED,
+ reInflateFlags = FLAG_CONTENT_VIEW_EXPANDED,
inflationId = 0,
remoteViewCache = mock(),
- entry = mRow.entry,
- row = mRow,
+ entry = row.entry,
+ row = row,
isNewView = true, /* isNewView */
remoteViewClickHandler = { _, _, _ -> true },
callback =
@@ -253,7 +282,7 @@
countDownLatch.countDown()
}
},
- parentLayout = mRow.privateLayout,
+ parentLayout = row.privateLayout,
existingView = null,
existingWrapper = null,
runningInflations = HashMap(),
@@ -275,13 +304,13 @@
@Test
fun doesntReapplyDisallowedRemoteView() {
- mBuilder.setStyle(Notification.MediaStyle())
- val mediaView = mBuilder.createContentView()
- mBuilder.setStyle(Notification.DecoratedCustomViewStyle())
- mBuilder.setCustomContentView(
+ builder.setStyle(Notification.MediaStyle())
+ val mediaView = builder.createContentView()
+ builder.setStyle(Notification.DecoratedCustomViewStyle())
+ builder.setCustomContentView(
RemoteViews(context.packageName, com.android.systemui.tests.R.layout.custom_view_dark)
)
- val decoratedMediaView = mBuilder.createContentView()
+ val decoratedMediaView = builder.createContentView()
Assert.assertFalse(
"The decorated media style doesn't allow a view to be reapplied!",
NotificationRowContentBinderImpl.canReapplyRemoteView(mediaView, decoratedMediaView)
@@ -292,112 +321,167 @@
@Ignore("b/345418902")
fun testUsesSameViewWhenCachedPossibleToReuse() {
// GIVEN a cached view.
- val contractedRemoteView = mBuilder.createContentView()
- whenever(
- mCache.hasCachedView(
- mRow.entry,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
- )
- )
- .thenReturn(true)
- whenever(
- mCache.getCachedView(
- mRow.entry,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
- )
- )
+ val contractedRemoteView = builder.createContentView()
+ whenever(cache.hasCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)).thenReturn(true)
+ whenever(cache.getCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED))
.thenReturn(contractedRemoteView)
// GIVEN existing bound view with same layout id.
val view = contractedRemoteView.apply(mContext, null /* parent */)
- mRow.privateLayout.setContractedChild(view)
+ row.privateLayout.setContractedChild(view)
// WHEN inflater inflates
- inflateAndWait(
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED,
- mRow
- )
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
// THEN the view should be re-used
Assert.assertEquals(
"Binder inflated a new view even though the old one was cached and usable.",
view,
- mRow.privateLayout.contractedChild
+ row.privateLayout.contractedChild
)
}
@Test
fun testInflatesNewViewWhenCachedNotPossibleToReuse() {
// GIVEN a cached remote view.
- val contractedRemoteView = mBuilder.createHeadsUpContentView()
- whenever(
- mCache.hasCachedView(
- mRow.entry,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
- )
- )
- .thenReturn(true)
- whenever(
- mCache.getCachedView(
- mRow.entry,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
- )
- )
+ val contractedRemoteView = builder.createHeadsUpContentView()
+ whenever(cache.hasCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED)).thenReturn(true)
+ whenever(cache.getCachedView(row.entry, FLAG_CONTENT_VIEW_CONTRACTED))
.thenReturn(contractedRemoteView)
// GIVEN existing bound view with different layout id.
val view: View = TextView(mContext)
- mRow.privateLayout.setContractedChild(view)
+ row.privateLayout.setContractedChild(view)
// WHEN inflater inflates
- inflateAndWait(
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED,
- mRow
- )
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
// THEN the view should be a new view
Assert.assertNotEquals(
"Binder (somehow) used the same view when inflating.",
view,
- mRow.privateLayout.contractedChild
+ row.privateLayout.contractedChild
)
}
@Test
fun testInflationCachesCreatedRemoteView() {
// WHEN inflater inflates
- inflateAndWait(
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED,
- mRow
- )
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
// THEN inflater informs cache of the new remote view
- verify(mCache)
- .putCachedView(
- eq(mRow.entry),
- eq(NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED),
- any()
- )
+ verify(cache).putCachedView(eq(row.entry), eq(FLAG_CONTENT_VIEW_CONTRACTED), any())
}
@Test
fun testUnbindRemovesCachedRemoteView() {
// WHEN inflated unbinds content
- mNotificationInflater.unbindContent(
- mRow.entry,
- mRow,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP
- )
+ notificationInflater.unbindContent(row.entry, row, FLAG_CONTENT_VIEW_HEADS_UP)
// THEN inflated informs cache to remove remote view
- verify(mCache)
- .removeCachedView(
- eq(mRow.entry),
- eq(NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP)
- )
+ verify(cache).removeCachedView(eq(row.entry), eq(FLAG_CONTENT_VIEW_HEADS_UP))
+ }
+
+ @Test
+ fun testRonModelRequiredForRonView() {
+ fakeRonContentModel = null
+ val ronView = View(context)
+ fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mock())
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+ verify(fakeRonViewInflater, never()).inflateView(any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun testRonModelTriggersInflationOfRonView() {
+ val mockRonModel = mock<TimerContentModel>()
+ val ronView = View(context)
+ val mockBinder = mock<DeferredContentViewBinder>()
+
+ val entry = row.entry
+ val privateLayout = row.privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+ // VERIFY that the inflater is invoked
+ verify(fakeRonViewInflater)
+ .inflateView(eq(mockRonModel), any(), eq(entry), any(), eq(privateLayout))
+ assertThat(row.privateLayout.contractedChild).isSameInstanceAs(ronView)
+ verify(mockBinder).setupContentViewBinder()
+ }
+
+ @Test
+ fun ronViewAppliesElementsInOrder() {
+ val oldHandle = mock<DisposableHandle>()
+ val mockRonModel = mock<TimerContentModel>()
+ val ronView = View(context)
+ val mockBinder = mock<DeferredContentViewBinder>()
+
+ row.privateLayout.mContractedBinderHandle = oldHandle
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ fakeRonContentModel = mockRonModel
+ fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder)
+ // WHEN inflater inflates
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(oldHandle, entry, privateLayout, mockBinder) {
+ verify(oldHandle).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel })
+ verify(privateLayout).setContractedChild(eq(ronView))
+ verify(mockBinder).setupContentViewBinder()
+ }
+ }
+
+ @Test
+ fun testRonNotReinflating() {
+ val handle0 = mock<DisposableHandle>()
+ val handle1 = mock<DisposableHandle>()
+ val ronView = View(context)
+ val mockRonModel1 = mock<TimerContentModel>()
+ val mockRonModel2 = mock<TimerContentModel>()
+ val mockBinder1 = mock<DeferredContentViewBinder>()
+ doReturn(handle1).whenever(mockBinder1).setupContentViewBinder()
+
+ row.privateLayout.mContractedBinderHandle = handle0
+ val entry = spy(row.entry)
+ row.entry = entry
+ val privateLayout = spy(row.privateLayout)
+ row.privateLayout = privateLayout
+
+ // WHEN inflater inflates both a model and a view
+ fakeRonContentModel = mockRonModel1
+ fakeRonViewHolder = InflatedContentViewHolder(view = ronView, binder = mockBinder1)
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // Validate that these 4 steps happen in this precise order
+ inOrder(handle0, entry, privateLayout, mockBinder1, handle1) {
+ verify(handle0).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel1 })
+ verify(privateLayout).setContractedChild(eq(ronView))
+ verify(mockBinder1).setupContentViewBinder()
+ verify(handle1, never()).dispose()
+ }
+
+ clearInvocations(handle0, entry, privateLayout, mockBinder1, handle1)
+
+ // THEN when the inflater inflates just a model
+ fakeRonContentModel = mockRonModel2
+ fakeRonViewHolder = null
+ inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_CONTRACTED, row)
+
+ // Validate that for reinflation, the only thing we do us update the model
+ verify(handle1, never()).dispose()
+ verify(entry).setContentModel(argThat { richOngoingContentModel === mockRonModel2 })
+ verify(privateLayout, never()).setContractedChild(any())
+ verify(mockBinder1, never()).setupContentViewBinder()
+ verify(handle1, never()).dispose()
}
@Test
@@ -453,46 +537,36 @@
whenever(view.measuredHeight)
.thenReturn(
TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
+ COMPLEX_UNIT_SP,
measuredHeightDp,
mContext.resources.displayMetrics
)
.toInt()
)
- mRow.entry.targetSdk = targetSdk
- mRow.entry.sbn.notification.contentView = contentView
- return NotificationRowContentBinderImpl.isValidView(view, mRow.entry, mContext.resources)
+ row.entry.targetSdk = targetSdk
+ row.entry.sbn.notification.contentView = contentView
+ return NotificationRowContentBinderImpl.isValidView(view, row.entry, mContext.resources)
}
@Test
fun testInvalidNotificationDoesNotInvokeCallback() {
- mRow.privateLayout.removeAllViews()
- mRow.entry.sbn.notification.contentView =
+ row.privateLayout.removeAllViews()
+ row.entry.sbn.notification.contentView =
RemoteViews(
mContext.packageName,
com.android.systemui.tests.R.layout.invalid_notification_height
)
- inflateAndWait(
- true,
- mNotificationInflater,
- NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL,
- mRow
- )
- Assert.assertEquals(0, mRow.privateLayout.childCount.toLong())
- verify(mRow, times(0)).onNotificationUpdated()
+ inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, row)
+ Assert.assertEquals(0, row.privateLayout.childCount.toLong())
+ verify(row, times(0)).onNotificationUpdated()
}
private class ExceptionHolder {
- var mException: Exception? = null
-
- fun setException(exception: Exception?) {
- mException = exception
- }
+ var exception: Exception? = null
}
private class AsyncFailRemoteView(packageName: String?, layoutId: Int) :
RemoteViews(packageName, layoutId) {
- var mHandler = mockExecutorHandler { p0 -> p0.run() }
override fun apply(context: Context, parent: ViewGroup): View {
return super.apply(context, parent)
@@ -505,7 +579,7 @@
listener: OnViewAppliedListener,
handler: InteractionHandler?
): CancellationSignal {
- mHandler.post { listener.onError(RuntimeException("Failed to inflate async")) }
+ executor.execute { listener.onError(RuntimeException("Failed to inflate async")) }
return CancellationSignal()
}
@@ -541,18 +615,17 @@
object : InflationCallback {
override fun handleInflationException(entry: NotificationEntry, e: Exception) {
if (!expectingException) {
- exceptionHolder.setException(e)
+ exceptionHolder.exception = e
}
countDownLatch.countDown()
}
override fun onAsyncInflationFinished(entry: NotificationEntry) {
if (expectingException) {
- exceptionHolder.setException(
+ exceptionHolder.exception =
RuntimeException(
"Inflation finished even though there should be an error"
)
- )
}
countDownLatch.countDown()
}
@@ -566,7 +639,7 @@
callback /* callback */
)
Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS))
- exceptionHolder.mException?.let { throw it }
+ exceptionHolder.exception?.let { throw it }
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 21d586b..c74a04f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -199,6 +199,8 @@
mock(NotifRemoteViewCache.class),
mock(NotificationRemoteInputManager.class),
mock(ConversationNotificationProcessor.class),
+ mock(RichOngoingNotificationContentExtractor.class),
+ mock(RichOngoingNotificationViewInflater.class),
mock(Executor.class),
new MockSmartReplyInflater(),
mock(NotifLayoutInflaterFactory.Provider.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 9b0fd96..c1f2cb77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -367,14 +367,14 @@
@EnableSceneContainer
fun updateState_withViewInShelf_showShelf() {
// GIVEN a view is scrolled into the shelf
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
- val shelfTop = stackCutoff - scrimPadding - shelf.height
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
+ val shelfTop = stackTop + stackHeight - shelf.height
val stackScrollAlgorithmState = StackScrollAlgorithmState()
val viewInShelf = mock(ExpandableView::class.java)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
whenever(ambientState.isShadeExpanded).thenReturn(true)
whenever(ambientState.lastVisibleBackgroundChild).thenReturn(viewInShelf)
whenever(viewInShelf.viewState).thenReturn(ExpandableViewState())
@@ -401,57 +401,14 @@
@Test
@EnableSceneContainer
- fun updateState_withViewInShelfDuringExpansion_showShelf() {
- // GIVEN a view is scrolled into the shelf
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
- val stackBottom = stackCutoff - scrimPadding
- val shelfTop = stackBottom - shelf.height
- val stackScrollAlgorithmState = StackScrollAlgorithmState()
- val viewInShelf = mock(ExpandableView::class.java)
-
- // AND a shade expansion is in progress
- val shadeExpansionFraction = 0.5f
-
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
- whenever(ambientState.isShadeExpanded).thenReturn(true)
- whenever(ambientState.lastVisibleBackgroundChild).thenReturn(viewInShelf)
- whenever(ambientState.isExpansionChanging).thenReturn(true)
- whenever(ambientState.expansionFraction).thenReturn(shadeExpansionFraction)
- whenever(viewInShelf.viewState).thenReturn(ExpandableViewState())
- whenever(viewInShelf.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
- whenever(viewInShelf.translationY).thenReturn(shelfTop)
- whenever(viewInShelf.actualHeight).thenReturn(10)
- whenever(viewInShelf.isInShelf).thenReturn(true)
- whenever(viewInShelf.minHeight).thenReturn(10)
- whenever(viewInShelf.shelfTransformationTarget).thenReturn(null) // use translationY
- whenever(viewInShelf.isInShelf).thenReturn(true)
-
- stackScrollAlgorithmState.visibleChildren.add(viewInShelf)
- stackScrollAlgorithmState.firstViewInShelf = viewInShelf
-
- // WHEN Shelf's ViewState is updated
- shelf.updateState(stackScrollAlgorithmState, ambientState)
-
- // THEN the shelf is visible
- val shelfState = shelf.viewState as NotificationShelf.ShelfState
- assertEquals(false, shelfState.hidden)
- assertEquals(shelf.height, shelfState.height)
- // AND its translation is scaled by the shade expansion
- assertEquals((stackBottom * 0.75f) - shelf.height, shelfState.yTranslation)
- }
-
- @Test
- @EnableSceneContainer
fun updateState_withNullLastVisibleBackgroundChild_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
whenever(ambientState.isShadeExpanded).thenReturn(true)
val lastVisibleBackgroundChild = mock<ExpandableView>()
val expandableViewState = ExpandableViewState()
@@ -467,7 +424,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
@@ -501,12 +458,12 @@
@EnableSceneContainer
fun updateState_withNullFirstViewInShelf_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
whenever(ambientState.isShadeExpanded).thenReturn(true)
val lastVisibleBackgroundChild = mock<ExpandableView>()
val expandableViewState = ExpandableViewState()
@@ -522,7 +479,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
@@ -556,12 +513,12 @@
@EnableSceneContainer
fun updateState_withCollapsedShade_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
val lastVisibleBackgroundChild = mock<ExpandableView>()
val expandableViewState = ExpandableViewState()
whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
@@ -577,7 +534,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
@@ -609,12 +566,12 @@
@Test
@EnableSceneContainer
- fun updateState_withHiddenSectionBeforeShelf_hideShelf_withSceneContianer() {
+ fun updateState_withHiddenSectionBeforeShelf_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
whenever(ambientState.isShadeExpanded).thenReturn(true)
@@ -646,7 +603,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 967f95e..770c424 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -262,15 +262,62 @@
}
@Test
- public void updateStackEndHeightAndStackHeight_normallyUpdatesBoth() {
- final float expansionFraction = 0.5f;
+ @EnableSceneContainer
+ public void updateStackEndHeightAndStackHeight_shadeFullyExpanded_withSceneContainer() {
+ final float stackTop = 200f;
+ final float stackCutoff = 1000f;
+ final float stackEndHeight = stackCutoff - stackTop;
+ mAmbientState.setStackTop(stackTop);
+ mAmbientState.setStackCutoff(stackCutoff);
mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
-
- // Validate that by default we update everything
clearInvocations(mAmbientState);
+
+ // WHEN shade is fully expanded
+ mStackScroller.updateStackEndHeightAndStackHeight(/* fraction = */ 1.0f);
+
+ // THEN stackHeight and stackEndHeight are the same
+ verify(mAmbientState).setStackEndHeight(stackEndHeight);
+ verify(mAmbientState).setStackHeight(stackEndHeight);
+ }
+
+ @Test
+ @EnableSceneContainer
+ public void updateStackEndHeightAndStackHeight_shadeExpanding_withSceneContainer() {
+ final float stackTop = 200f;
+ final float stackCutoff = 1000f;
+ final float stackEndHeight = stackCutoff - stackTop;
+ mAmbientState.setStackTop(stackTop);
+ mAmbientState.setStackCutoff(stackCutoff);
+ mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
+ clearInvocations(mAmbientState);
+
+ // WHEN shade is expanding
+ final float expansionFraction = 0.5f;
mStackScroller.updateStackEndHeightAndStackHeight(expansionFraction);
- verify(mAmbientState).setStackEndHeight(anyFloat());
- verify(mAmbientState).setStackHeight(anyFloat());
+
+ // THEN stackHeight is changed by the expansion frac
+ verify(mAmbientState).setStackEndHeight(stackEndHeight);
+ verify(mAmbientState).setStackHeight(stackEndHeight * 0.75f);
+ }
+
+ @Test
+ @EnableSceneContainer
+ public void updateStackEndHeightAndStackHeight_shadeOverscrolledToTop_withSceneContainer() {
+ // GIVEN stack scrolled over the top, stack top is negative
+ final float stackTop = -2000f;
+ final float stackCutoff = 1000f;
+ final float stackEndHeight = stackCutoff - stackTop;
+ mAmbientState.setStackTop(stackTop);
+ mAmbientState.setStackCutoff(stackCutoff);
+ mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
+ clearInvocations(mAmbientState);
+
+ // WHEN stack is updated
+ mStackScroller.updateStackEndHeightAndStackHeight(/* fraction = */ 1.0f);
+
+ // THEN stackHeight is measured from the stack top
+ verify(mAmbientState).setStackEndHeight(stackEndHeight);
+ verify(mAmbientState).setStackHeight(stackEndHeight);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 8401a19..b12c098 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -8,6 +8,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
import com.android.systemui.dump.DumpManager
@@ -549,6 +550,7 @@
assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
}
+ @DisableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
@Test
fun resetViewStates_clearAllInProgress_allRowsRemoved_emptyShade_footerHidden() {
ambientState.isClearAllInProgress = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index f2f336c..dfee2ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -21,11 +21,14 @@
import android.app.admin.DevicePolicyResourcesManager
import android.content.SharedPreferences
import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.telecom.TelecomManager
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -39,6 +42,7 @@
import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.policy.BluetoothController
import com.android.systemui.statusbar.policy.CastController
+import com.android.systemui.statusbar.policy.CastDevice
import com.android.systemui.statusbar.policy.DataSaverController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.HotspotController
@@ -54,6 +58,7 @@
import com.android.systemui.util.kotlin.JavaAdapter
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.DateFormatUtil
import com.android.systemui.util.time.FakeSystemClock
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -78,6 +83,7 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
@RunWith(AndroidJUnit4::class)
@RunWithLooper
@@ -87,6 +93,8 @@
companion object {
private const val ALARM_SLOT = "alarm"
+ private const val CAST_SLOT = "cast"
+ private const val SCREEN_RECORD_SLOT = "screen_record"
private const val CONNECTED_DISPLAY_SLOT = "connected_display"
private const val MANAGED_PROFILE_SLOT = "managed_profile"
}
@@ -271,6 +279,101 @@
verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
}
+ @Test
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+ fun cast_chipsFlagOff_iconShown() {
+ statusBarPolicy.init()
+ clearInvocations(iconController)
+
+ val callbackCaptor = argumentCaptor<CastController.Callback>()
+ verify(castController).addCallback(callbackCaptor.capture())
+
+ whenever(castController.castDevices)
+ .thenReturn(
+ listOf(
+ CastDevice(
+ "id",
+ "name",
+ "description",
+ CastDevice.CastState.Connected,
+ CastDevice.CastOrigin.MediaProjection,
+ )
+ )
+ )
+ callbackCaptor.firstValue.onCastDevicesChanged()
+
+ verify(iconController).setIconVisibility(CAST_SLOT, true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+ fun cast_chipsFlagOn_noCallbackRegistered() {
+ statusBarPolicy.init()
+
+ verify(castController, never()).addCallback(any())
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+ fun screenRecord_chipsFlagOff_iconShown_forAllStates() {
+ statusBarPolicy.init()
+ clearInvocations(iconController)
+
+ val callbackCaptor = argumentCaptor<RecordingController.RecordingStateChangeCallback>()
+ verify(recordingController).addCallback(callbackCaptor.capture())
+
+ callbackCaptor.firstValue.onCountdown(3000)
+ testableLooper.processAllMessages()
+ verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
+ clearInvocations(iconController)
+
+ callbackCaptor.firstValue.onCountdownEnd()
+ testableLooper.processAllMessages()
+ verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
+ clearInvocations(iconController)
+
+ callbackCaptor.firstValue.onRecordingStart()
+ testableLooper.processAllMessages()
+ verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
+ clearInvocations(iconController)
+
+ callbackCaptor.firstValue.onRecordingEnd()
+ testableLooper.processAllMessages()
+ verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
+ clearInvocations(iconController)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+ fun screenRecord_chipsFlagOn_noCallbackRegistered() {
+ statusBarPolicy.init()
+
+ verify(recordingController, never()).addCallback(any())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+ fun screenRecord_chipsFlagOn_methodsDoNothing() {
+ statusBarPolicy.init()
+ clearInvocations(iconController)
+
+ statusBarPolicy.onCountdown(3000)
+ testableLooper.processAllMessages()
+ verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+ statusBarPolicy.onCountdownEnd()
+ testableLooper.processAllMessages()
+ verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+ statusBarPolicy.onRecordingStart()
+ testableLooper.processAllMessages()
+ verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+ statusBarPolicy.onRecordingEnd()
+ testableLooper.processAllMessages()
+ verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+ }
+
private fun createAlarmInfo(): AlarmManager.AlarmClockInfo {
return AlarmManager.AlarmClockInfo(10L, null)
}
@@ -315,13 +418,18 @@
private class FakeConnectedDisplayStateProvider : ConnectedDisplayInteractor {
private val flow = MutableSharedFlow<State>()
+
suspend fun emit(value: State) = flow.emit(value)
+
override val connectedDisplayState: Flow<State>
get() = flow
+
override val connectedDisplayAddition: Flow<Unit>
get() = TODO("Not yet implemented")
+
override val pendingDisplay: Flow<PendingDisplay?>
get() = TODO("Not yet implemented")
+
override val concurrentDisplaysInProgress: Flow<Boolean>
get() = TODO("Not yet implemented")
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index fabb9b7..c5fbc39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -25,6 +25,8 @@
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
+import static androidx.test.ext.truth.content.IntentSubject.assertThat;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR;
@@ -2017,6 +2019,31 @@
}
@Test
+ public void testShowOrHideAppBubble_updateExistedBubbleInOverflow_updateIntentInBubble() {
+ String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0);
+ mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ // Collapse the stack so we don't need to wait for the dismiss animation in the test
+ mBubbleController.collapseStack();
+ // Dismiss the app bubble so it's in the overflow
+ mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE);
+ assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull();
+
+ // Modify the intent to include new extras.
+ Intent newAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class)
+ .setPackage(mContext.getPackageName())
+ .putExtra("hello", "world");
+
+ // Calling this while collapsed will re-add and expand the app bubble
+ mBubbleController.showOrHideAppBubble(newAppBubbleIntent, mUser0, mAppBubbleIcon);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+ assertThat(mBubbleController.isStackExpanded()).isTrue();
+ assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
+ assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent()).extras().string(
+ "hello").isEqualTo("world");
+ assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull();
+ }
+
+ @Test
public void testCreateBubbleFromOngoingNotification() {
NotificationEntry notif = new NotificationEntryBuilder()
.setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
new file mode 100644
index 0000000..36ac4a4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/FakeInputManager.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.hardware.input
+
+import android.view.InputDevice
+import android.view.KeyCharacterMap
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.invocation.InvocationOnMock
+
+class FakeInputManager {
+
+ private val devices = mutableMapOf<Int, InputDevice>()
+
+ val inputManager =
+ mock<InputManager> {
+ whenever(getInputDevice(anyInt())).thenAnswer { invocation ->
+ val deviceId = invocation.arguments[0] as Int
+ return@thenAnswer devices[deviceId]
+ }
+ whenever(inputDeviceIds).thenAnswer {
+ return@thenAnswer devices.keys.toIntArray()
+ }
+
+ fun setDeviceEnabled(invocation: InvocationOnMock, enabled: Boolean) {
+ val deviceId = invocation.arguments[0] as Int
+ val device = devices[deviceId] ?: return
+ devices[deviceId] = device.copy(enabled = enabled)
+ }
+
+ whenever(disableInputDevice(anyInt())).thenAnswer { invocation ->
+ setDeviceEnabled(invocation, enabled = false)
+ }
+ whenever(enableInputDevice(anyInt())).thenAnswer { invocation ->
+ setDeviceEnabled(invocation, enabled = true)
+ }
+ }
+
+ fun addPhysicalKeyboard(id: Int, enabled: Boolean = true) {
+ check(id > 0) { "Physical keyboard ids have to be > 0" }
+ addKeyboard(id, enabled)
+ }
+
+ fun addVirtualKeyboard(enabled: Boolean = true) {
+ addKeyboard(id = KeyCharacterMap.VIRTUAL_KEYBOARD, enabled)
+ }
+
+ private fun addKeyboard(id: Int, enabled: Boolean = true) {
+ devices[id] =
+ InputDevice.Builder()
+ .setId(id)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setSources(InputDevice.SOURCE_KEYBOARD)
+ .setEnabled(enabled)
+ .build()
+ }
+
+ private fun InputDevice.copy(
+ id: Int = getId(),
+ type: Int = keyboardType,
+ sources: Int = getSources(),
+ enabled: Boolean = isEnabled
+ ) =
+ InputDevice.Builder()
+ .setId(id)
+ .setKeyboardType(type)
+ .setSources(sources)
+ .setEnabled(enabled)
+ .build()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/android/hardware/input/InputManagerKosmos.kt
similarity index 81%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/android/hardware/input/InputManagerKosmos.kt
index d8af3fa..bf68eab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/input/InputManagerKosmos.kt
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.data.repository
+package android.hardware.input
import com.android.systemui.kosmos.Kosmos
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.fakeInputManager by Kosmos.Fixture { FakeInputManager() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 9dae44d..5db9d31 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui
import android.app.ActivityManager
+import android.app.DreamManager
import android.app.admin.DevicePolicyManager
import android.app.trust.TrustManager
import android.os.UserManager
@@ -32,6 +33,7 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.ScreenLifecycle
@@ -93,6 +95,7 @@
@get:Provides val demoModeController: DemoModeController = mock(),
@get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
@get:Provides val dozeParameters: DozeParameters = mock(),
+ @get:Provides val dreamManager: DreamManager = mock(),
@get:Provides val dumpManager: DumpManager = mock(),
@get:Provides val headsUpManager: HeadsUpManager = mock(),
@get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(),
@@ -130,6 +133,7 @@
@get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(),
@get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(),
@get:Provides val communalInteractor: CommunalInteractor = mock(),
+ @get:Provides val communalSceneInteractor: CommunalSceneInteractor = mock(),
@get:Provides val sceneLogger: SceneLogger = mock(),
@get:Provides val trustManager: TrustManager = mock(),
@get:Provides val primaryBouncerInteractor: PrimaryBouncerInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 3401cc4..0b12936 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyboard.shortcut
import android.content.applicationContext
+import android.hardware.input.fakeInputManager
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperRepository
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper
@@ -32,7 +33,15 @@
import com.android.systemui.settings.displayTracker
val Kosmos.shortcutHelperRepository by
- Kosmos.Fixture { ShortcutHelperRepository(fakeCommandQueue, broadcastDispatcher) }
+ Kosmos.Fixture {
+ ShortcutHelperRepository(
+ fakeCommandQueue,
+ broadcastDispatcher,
+ fakeInputManager.inputManager,
+ testScope,
+ testDispatcher
+ )
+ }
val Kosmos.shortcutHelperTestHelper by
Kosmos.Fixture {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index dca531a..5bae6ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -133,6 +133,8 @@
override val topClippingBounds = MutableStateFlow<Int?>(null)
+ private var isShowKeyguardWhenReenabled: Boolean = false
+
override fun setQuickSettingsVisible(isVisible: Boolean) {
_isQuickSettingsVisible.value = isVisible
}
@@ -268,6 +270,14 @@
fun setIsEncryptedOrLockdown(value: Boolean) {
_isEncryptedOrLockdown.value = value
}
+
+ override fun setShowKeyguardWhenReenabled(isShowKeyguardWhenReenabled: Boolean) {
+ this.isShowKeyguardWhenReenabled = isShowKeyguardWhenReenabled
+ }
+
+ override fun isShowKeyguardWhenReenabled(): Boolean {
+ return isShowKeyguardWhenReenabled
+ }
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
index cd83c2f..39a1a63 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
@@ -33,9 +33,10 @@
private val _isTrustUsuallyManaged = MutableStateFlow(false)
override val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
get() = _isTrustUsuallyManaged
+
private val _isCurrentUserTrusted = MutableStateFlow(false)
- override val isCurrentUserTrusted: Flow<Boolean>
- get() = _isCurrentUserTrusted
+ override val isCurrentUserTrusted: StateFlow<Boolean>
+ get() = _isCurrentUserTrusted.asStateFlow()
private val _isCurrentUserActiveUnlockAvailable = MutableStateFlow(false)
override val isCurrentUserActiveUnlockRunning: StateFlow<Boolean> =
@@ -48,6 +49,13 @@
private val _requestDismissKeyguard = MutableStateFlow(TrustModel(false, 0, TrustGrantFlags(0)))
override val trustAgentRequestingToDismissKeyguard: Flow<TrustModel> = _requestDismissKeyguard
+ var keyguardShowingChangeEventCount: Int = 0
+ private set
+
+ override suspend fun reportKeyguardShowingChanged() {
+ keyguardShowingChangeEventCount++
+ }
+
fun setCurrentUserTrusted(trust: Boolean) {
_isCurrentUserTrusted.value = trust
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index edf77a0..744b127 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -16,7 +16,9 @@
package com.android.systemui.keyguard.domain.interactor
+import android.service.dream.dreamManager
import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
@@ -35,8 +37,10 @@
mainDispatcher = testDispatcher,
keyguardInteractor = keyguardInteractor,
communalInteractor = communalInteractor,
+ communalSceneInteractor = communalSceneInteractor,
powerInteractor = powerInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
deviceEntryRepository = deviceEntryRepository,
+ dreamManager = dreamManager
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt
index 0ebf164..d60326c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/TrustInteractorKosmos.kt
@@ -19,5 +19,11 @@
import com.android.systemui.keyguard.data.repository.trustRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
-val Kosmos.trustInteractor by Fixture { TrustInteractor(repository = trustRepository) }
+val Kosmos.trustInteractor by Fixture {
+ TrustInteractor(
+ applicationScope = applicationCoroutineScope,
+ repository = trustRepository,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
similarity index 88%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
index d8af3fa..2f5daaa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
@@ -18,4 +18,4 @@
import com.android.systemui.kosmos.Kosmos
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.fixedColumnsRepository by Kosmos.Fixture { FixedColumnsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
similarity index 69%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
index d8af3fa..696c4bf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
@@ -16,6 +16,12 @@
package com.android.systemui.qs.panels.data.repository
+import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.paginatedGridRepository by
+ Kosmos.Fixture {
+ testCase.context.orCreateTestableResources
+ PaginatedGridRepository(testCase.context.resources, configurationRepository)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
similarity index 78%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
index 6e11977..f4d281d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.fixedColumnsRepository
-val Kosmos.infiniteGridSizeInteractor by
- Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+val Kosmos.fixedColumnsSizeInteractor by
+ Kosmos.Fixture { FixedColumnsSizeInteractor(fixedColumnsRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
index 7f387d7..320c2ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
@@ -20,5 +20,5 @@
val Kosmos.infiniteGridConsistencyInteractor by
Kosmos.Fixture {
- InfiniteGridConsistencyInteractor(iconTilesInteractor, infiniteGridSizeInteractor)
+ InfiniteGridConsistencyInteractor(iconTilesInteractor, fixedColumnsSizeInteractor)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index 82cfaf5..be00152 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -18,8 +18,8 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.infiniteGridSizeViewModel
val Kosmos.infiniteGridLayout by
- Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, infiniteGridSizeViewModel) }
+ Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
similarity index 78%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
index 6e11977..a922e5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.paginatedGridRepository
-val Kosmos.infiniteGridSizeInteractor by
- Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+val Kosmos.paginatedGridInteractor by
+ Kosmos.Fixture { PaginatedGridInteractor(paginatedGridRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
similarity index 79%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
index f6dfb8b..feadc91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.fixedColumnsSizeInteractor
-val Kosmos.infiniteGridSizeViewModel by
- Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
+val Kosmos.fixedColumnsSizeViewModel by
+ Kosmos.Fixture { FixedColumnsSizeViewModelImpl(fixedColumnsSizeInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt
new file mode 100644
index 0000000..5386ece
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.qs.panels.ui.viewmodel
+
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+fun MockTileViewModel(
+ spec: TileSpec,
+ state: StateFlow<QSTile.State> = MutableStateFlow(QSTile.State())
+): TileViewModel = mock {
+ whenever(this.spec).thenReturn(spec)
+ whenever(this.state).thenReturn(state)
+ whenever(this.currentState).thenReturn(state.value)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
index f6dfb8b..85e9265 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
@@ -17,7 +17,16 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor
-val Kosmos.infiniteGridSizeViewModel by
- Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
+val Kosmos.paginatedGridViewModel by
+ Kosmos.Fixture {
+ PaginatedGridViewModel(
+ iconTilesViewModel,
+ fixedColumnsSizeViewModel,
+ iconLabelVisibilityViewModel,
+ paginatedGridInteractor,
+ applicationCoroutineScope,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
index b07cc7d..fde174d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
@@ -22,7 +22,7 @@
Kosmos.Fixture {
PartitionedGridViewModel(
iconTilesViewModel,
- infiniteGridSizeViewModel,
+ fixedColumnsSizeViewModel,
iconLabelVisibilityViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
index 066736c..0921eb9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.scene.domain.interactor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.scene.data.repository.sceneContainerRepository
@@ -31,5 +32,6 @@
logger = sceneLogger,
sceneFamilyResolvers = { sceneFamilyResolvers },
deviceUnlockedInteractor = deviceUnlockedInteractor,
+ keyguardEnabledInteractor = keyguardEnabledInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
index 6be1939..ae33aea 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt
@@ -20,6 +20,7 @@
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.scene.shared.model.SceneFamilies
@@ -39,6 +40,7 @@
HomeSceneFamilyResolver(
applicationScope = applicationCoroutineScope,
deviceEntryInteractor = deviceEntryInteractor,
+ keyguardEnabledInteractor = keyguardEnabledInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableKosmos.kt
new file mode 100644
index 0000000..f9111bf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/KeyguardStateCallbackStartableKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.scene.domain.startable
+
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.keyguard.domain.interactor.trustInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+
+val Kosmos.keyguardStateCallbackStartable by Fixture {
+ KeyguardStateCallbackStartable(
+ applicationScope = applicationCoroutineScope,
+ backgroundDispatcher = testDispatcher,
+ sceneInteractor = sceneInteractor,
+ selectedUserInteractor = selectedUserInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ trustInteractor = trustInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
similarity index 89%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index cf18c0e..a661ab6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.scene.domain.interactor
+package com.android.systemui.scene.domain.startable
import com.android.internal.logging.uiEventLogger
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
@@ -25,6 +25,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.windowManagerLockscreenVisibilityInteractor
import com.android.systemui.kosmos.Kosmos
@@ -32,7 +33,9 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.sysUiState
import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.scene.domain.startable.SceneContainerStartable
+import com.android.systemui.scene.domain.interactor.sceneBackInteractor
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.session.shared.shadeSessionStorage
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.settings.displayTracker
@@ -69,5 +72,6 @@
sceneBackInteractor = sceneBackInteractor,
shadeSessionStorage = shadeSessionStorage,
windowMgrLockscreenVisInteractor = windowManagerLockscreenVisibilityInteractor,
+ keyguardEnabledInteractor = keyguardEnabledInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 4813794..b9918f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -30,7 +30,7 @@
@SysUISingleton
class FakeShadeRepository @Inject constructor() : ShadeRepository {
private val _qsExpansion = MutableStateFlow(0f)
- override val qsExpansion = _qsExpansion
+ @Deprecated("Use ShadeInteractor.qsExpansion instead") override val qsExpansion = _qsExpansion
private val _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f)
override val udfpsTransitionToFullShadeProgress = _udfpsTransitionToFullShadeProgress
@@ -59,12 +59,15 @@
private val _legacyIsQsExpanded = MutableStateFlow(false)
@Deprecated("Use ShadeInteractor instead") override val legacyIsQsExpanded = _legacyIsQsExpanded
+ @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead")
override val legacyLockscreenShadeTracking = MutableStateFlow(false)
private val _shadeMode = MutableStateFlow<ShadeMode>(ShadeMode.Single)
override val shadeMode: StateFlow<ShadeMode> = _shadeMode.asStateFlow()
- override val isDualShadeAlignedToBottom = false
+ private var _isDualShadeAlignedToBottom = false
+ override val isDualShadeAlignedToBottom
+ get() = _isDualShadeAlignedToBottom
@Deprecated("Use ShadeInteractor instead")
override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) {
@@ -139,8 +142,12 @@
_legacyShadeExpansion.value = expandedFraction
}
- override fun setShadeMode(shadeMode: ShadeMode) {
- _shadeMode.value = shadeMode
+ override fun setShadeMode(mode: ShadeMode) {
+ _shadeMode.value = mode
+ }
+
+ fun setDualShadeAlignedToBottom(isAlignedToBottom: Boolean) {
+ _isDualShadeAlignedToBottom = isAlignedToBottom
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
index 1ca3509..72a80d4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
@@ -18,6 +18,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.shade.domain.interactor.shadeInteractor
val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by
- Kosmos.Fixture { NotificationsShadeSceneViewModel() }
+ Kosmos.Fixture { NotificationsShadeSceneViewModel(shadeInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
index 4d81ea1..8adb26f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
@@ -22,6 +22,7 @@
import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModel
import com.android.systemui.qs.ui.adapter.qsSceneAdapter
import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel
+import com.android.systemui.shade.domain.interactor.shadeInteractor
val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by
Kosmos.Fixture {
@@ -31,5 +32,6 @@
tileGridViewModel = tileGridViewModel,
editModeViewModel = editModeViewModel,
qsSceneAdapter = qsSceneAdapter,
+ shadeInteractor = shadeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
new file mode 100644
index 0000000..16dc50f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -0,0 +1,390 @@
+/*
+ * 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.statusbar.notification.row
+
+import android.app.ActivityManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import androidx.core.os.bundleOf
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.statusbar.IStatusBarService
+import com.android.systemui.TestableDependency
+import com.android.systemui.classifier.FalsingManagerFake
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.media.dialog.MediaOutputDialogManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.shared.system.DevicePolicyManagerWrapper
+import com.android.systemui.shared.system.PackageManagerWrapper
+import com.android.systemui.statusbar.NotificationMediaManager
+import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.SmartReplyController
+import com.android.systemui.statusbar.notification.ColorUpdateLogger
+import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager
+import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
+import com.android.systemui.statusbar.notification.icon.IconBuilder
+import com.android.systemui.statusbar.notification.icon.IconManager
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.CoordinateOnClickListener
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
+import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.SmartActionInflaterImpl
+import com.android.systemui.statusbar.policy.SmartReplyConstants
+import com.android.systemui.statusbar.policy.SmartReplyInflaterImpl
+import com.android.systemui.statusbar.policy.SmartReplyStateInflaterImpl
+import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent
+import com.android.systemui.util.Assert.runWithCurrentThreadAsMainThread
+import com.android.systemui.util.DeviceConfigProxyFake
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.wmshell.BubblesManager
+import com.google.common.util.concurrent.MoreExecutors
+import com.google.common.util.concurrent.SettableFuture
+import java.util.Optional
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.test.TestScope
+import org.junit.Assert.assertTrue
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+
+class ExpandableNotificationRowBuilder(
+ private val context: Context,
+ dependency: TestableDependency,
+ private val featureFlags: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic()
+) {
+
+ private val mMockLogger: ExpandableNotificationRowLogger
+ private val mStatusBarStateController: StatusBarStateController
+ private val mKeyguardBypassController: KeyguardBypassController
+ private val mGroupMembershipManager: GroupMembershipManager
+ private val mGroupExpansionManager: GroupExpansionManager
+ private val mHeadsUpManager: HeadsUpManager
+ private val mIconManager: IconManager
+ private val mContentBinder: NotificationRowContentBinder
+ private val mBindStage: RowContentBindStage
+ private val mBindPipeline: NotifBindPipeline
+ private val mBindPipelineEntryListener: NotifCollectionListener
+ private val mPeopleNotificationIdentifier: PeopleNotificationIdentifier
+ private val mOnUserInteractionCallback: OnUserInteractionCallback
+ private val mDismissibilityProvider: NotificationDismissibilityProvider
+ private val mSmartReplyController: SmartReplyController
+ private val mSmartReplyConstants: SmartReplyConstants
+ private val mTestScope: TestScope = TestScope()
+ private val mBgCoroutineContext = mTestScope.backgroundScope.coroutineContext
+ private val mMainCoroutineContext = mTestScope.coroutineContext
+ private val mFakeSystemClock = FakeSystemClock()
+ private val mMainExecutor = FakeExecutor(mFakeSystemClock)
+
+ init {
+ featureFlags.setDefault(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE)
+ featureFlags.setDefault(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)
+
+ dependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
+ dependency.injectMockDependency(NotificationMediaManager::class.java)
+ dependency.injectMockDependency(NotificationShadeWindowController::class.java)
+ dependency.injectMockDependency(MediaOutputDialogManager::class.java)
+
+ mMockLogger = Mockito.mock(ExpandableNotificationRowLogger::class.java)
+ mStatusBarStateController = Mockito.mock(StatusBarStateController::class.java)
+ mKeyguardBypassController = Mockito.mock(KeyguardBypassController::class.java)
+ mGroupMembershipManager = GroupMembershipManagerImpl()
+ mSmartReplyController = Mockito.mock(SmartReplyController::class.java)
+
+ val dumpManager = DumpManager()
+ mGroupExpansionManager = GroupExpansionManagerImpl(dumpManager, mGroupMembershipManager)
+ mHeadsUpManager = Mockito.mock(HeadsUpManager::class.java)
+ mIconManager =
+ IconManager(
+ Mockito.mock(CommonNotifCollection::class.java),
+ Mockito.mock(LauncherApps::class.java),
+ IconBuilder(context),
+ mTestScope,
+ mBgCoroutineContext,
+ mMainCoroutineContext,
+ )
+
+ mSmartReplyConstants =
+ SmartReplyConstants(
+ /* mainExecutor = */ mMainExecutor,
+ /* context = */ context,
+ /* deviceConfig = */ DeviceConfigProxyFake().apply {
+ setProperty(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.SSIN_SHOW_IN_HEADS_UP,
+ "true",
+ true
+ )
+ setProperty(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.SSIN_ENABLED,
+ "true",
+ true
+ )
+ setProperty(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.SSIN_REQUIRES_TARGETING_P,
+ "false",
+ true
+ )
+ }
+ )
+ val remoteViewsFactories = getNotifRemoteViewsFactoryContainer(featureFlags)
+ val remoteInputManager = Mockito.mock(NotificationRemoteInputManager::class.java)
+ val smartReplyStateInflater =
+ SmartReplyStateInflaterImpl(
+ constants = mSmartReplyConstants,
+ activityManagerWrapper = ActivityManagerWrapper.getInstance(),
+ packageManagerWrapper = PackageManagerWrapper.getInstance(),
+ devicePolicyManagerWrapper = DevicePolicyManagerWrapper.getInstance(),
+ smartRepliesInflater =
+ SmartReplyInflaterImpl(
+ constants = mSmartReplyConstants,
+ keyguardDismissUtil = mock(),
+ remoteInputManager = remoteInputManager,
+ smartReplyController = mSmartReplyController,
+ context = context
+ ),
+ smartActionsInflater =
+ SmartActionInflaterImpl(
+ constants = mSmartReplyConstants,
+ activityStarter = mock(),
+ smartReplyController = mSmartReplyController,
+ headsUpManager = mHeadsUpManager
+ )
+ )
+ val notifLayoutInflaterFactoryProvider =
+ object : NotifLayoutInflaterFactory.Provider {
+ override fun provide(
+ row: ExpandableNotificationRow,
+ layoutType: Int
+ ): NotifLayoutInflaterFactory =
+ NotifLayoutInflaterFactory(row, layoutType, remoteViewsFactories)
+ }
+ val conversationProcessor =
+ ConversationNotificationProcessor(
+ mock(),
+ mock(),
+ )
+ mContentBinder =
+ if (NotificationRowContentBinderRefactor.isEnabled)
+ NotificationRowContentBinderImpl(
+ mock(),
+ remoteInputManager,
+ conversationProcessor,
+ mock(),
+ mock(),
+ mock(),
+ smartReplyStateInflater,
+ notifLayoutInflaterFactoryProvider,
+ mock(),
+ mock(),
+ )
+ else
+ NotificationContentInflater(
+ mock(),
+ remoteInputManager,
+ conversationProcessor,
+ mock(),
+ mock(),
+ smartReplyStateInflater,
+ notifLayoutInflaterFactoryProvider,
+ mock(),
+ mock(),
+ )
+ mContentBinder.setInflateSynchronously(true)
+ mBindStage =
+ RowContentBindStage(
+ mContentBinder,
+ mock(),
+ mock(),
+ )
+
+ val collection = Mockito.mock(CommonNotifCollection::class.java)
+
+ mBindPipeline =
+ NotifBindPipeline(
+ collection,
+ Mockito.mock(NotifBindPipelineLogger::class.java),
+ NotificationEntryProcessorFactoryExecutorImpl(mMainExecutor),
+ )
+ mBindPipeline.setStage(mBindStage)
+
+ val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
+ Mockito.verify(collection).addCollectionListener(collectionListenerCaptor.capture())
+ mBindPipelineEntryListener = collectionListenerCaptor.value
+ mPeopleNotificationIdentifier = Mockito.mock(PeopleNotificationIdentifier::class.java)
+ mOnUserInteractionCallback = Mockito.mock(OnUserInteractionCallback::class.java)
+ mDismissibilityProvider = Mockito.mock(NotificationDismissibilityProvider::class.java)
+ val mFutureDismissalRunnable = Mockito.mock(Runnable::class.java)
+ whenever(
+ mOnUserInteractionCallback.registerFutureDismissal(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt()
+ )
+ )
+ .thenReturn(mFutureDismissalRunnable)
+ }
+
+ private fun getNotifRemoteViewsFactoryContainer(
+ featureFlags: FeatureFlags,
+ ): NotifRemoteViewsFactoryContainer {
+ return NotifRemoteViewsFactoryContainerImpl(
+ featureFlags,
+ PrecomputedTextViewFactory(),
+ BigPictureLayoutInflaterFactory(),
+ NotificationOptimizedLinearLayoutFactory(),
+ { Mockito.mock(NotificationViewFlipperFactory::class.java) },
+ )
+ }
+
+ fun createRow(notification: Notification): ExpandableNotificationRow {
+ val channel =
+ NotificationChannel(
+ notification.channelId,
+ notification.channelId,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ channel.isBlockable = true
+ val entry =
+ NotificationEntryBuilder()
+ .setPkg(PKG)
+ .setOpPkg(PKG)
+ .setId(123321)
+ .setUid(UID)
+ .setInitialPid(2000)
+ .setNotification(notification)
+ .setUser(USER_HANDLE)
+ .setPostTime(System.currentTimeMillis())
+ .setChannel(channel)
+ .build()
+
+ // it is for mitigating Rank building process.
+ if (notification.isConversationStyleNotification) {
+ val rb = RankingBuilder(entry.ranking)
+ rb.setIsConversation(true)
+ entry.ranking = rb.build()
+ }
+
+ return generateRow(entry, FLAG_CONTENT_VIEW_ALL)
+ }
+
+ private fun generateRow(
+ entry: NotificationEntry,
+ @InflationFlag extraInflationFlags: Int
+ ): ExpandableNotificationRow {
+ // NOTE: This flag is read when the ExpandableNotificationRow is inflated, so it needs to be
+ // set, but we do not want to override an existing value that is needed by a specific test.
+
+ val rowFuture: SettableFuture<ExpandableNotificationRow> = SettableFuture.create()
+ val rowInflaterTask =
+ RowInflaterTask(mFakeSystemClock, Mockito.mock(RowInflaterTaskLogger::class.java))
+ rowInflaterTask.inflate(context, null, entry, MoreExecutors.directExecutor()) { inflatedRow
+ ->
+ rowFuture.set(inflatedRow)
+ }
+ val row = rowFuture.get(1, TimeUnit.SECONDS)
+
+ entry.row = row
+ mIconManager.createIcons(entry)
+ mBindPipelineEntryListener.onEntryInit(entry)
+ mBindPipeline.manageRow(entry, row)
+ row.initialize(
+ entry,
+ Mockito.mock(RemoteInputViewSubcomponent.Factory::class.java),
+ APP_NAME,
+ entry.key,
+ mMockLogger,
+ mKeyguardBypassController,
+ mGroupMembershipManager,
+ mGroupExpansionManager,
+ mHeadsUpManager,
+ mBindStage,
+ Mockito.mock(OnExpandClickListener::class.java),
+ Mockito.mock(CoordinateOnClickListener::class.java),
+ FalsingManagerFake(),
+ mStatusBarStateController,
+ mPeopleNotificationIdentifier,
+ mOnUserInteractionCallback,
+ Optional.of(Mockito.mock(BubblesManager::class.java)),
+ Mockito.mock(NotificationGutsManager::class.java),
+ mDismissibilityProvider,
+ Mockito.mock(MetricsLogger::class.java),
+ Mockito.mock(NotificationChildrenContainerLogger::class.java),
+ Mockito.mock(ColorUpdateLogger::class.java),
+ mSmartReplyConstants,
+ mSmartReplyController,
+ featureFlags,
+ Mockito.mock(IStatusBarService::class.java)
+ )
+ row.setAboveShelfChangedListener { aboveShelf: Boolean -> }
+ mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags)
+ inflateAndWait(entry)
+ return row
+ }
+
+ private fun inflateAndWait(entry: NotificationEntry) {
+ val countDownLatch = CountDownLatch(1)
+ mBindStage.requestRebind(entry) { en: NotificationEntry? -> countDownLatch.countDown() }
+ runWithCurrentThreadAsMainThread(mMainExecutor::runAllReady)
+ assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS))
+ }
+
+ companion object {
+ private const val APP_NAME = "appName"
+ private const val PKG = "com.android.systemui"
+ private const val UID = 1000
+ private val USER_HANDLE = UserHandle.of(ActivityManager.getCurrentUser())
+
+ private const val IS_CONVERSATION_FLAG = "test.isConversation"
+
+ private val Notification.isConversationStyleNotification
+ get() = extras.getBoolean(IS_CONVERSATION_FLAG, false)
+
+ fun markAsConversation(builder: Notification.Builder) {
+ builder.addExtras(bundleOf(IS_CONVERSATION_FLAG to true))
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt
new file mode 100644
index 0000000..84ef4b5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/data/repository/NotificationRowRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.statusbar.notification.row.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
+import kotlinx.coroutines.flow.MutableStateFlow
+
+val Kosmos.fakeNotificationRowRepository by Fixture { FakeNotificationRowRepository() }
+
+class FakeNotificationRowRepository : NotificationRowRepository {
+ override val richOngoingContentModel = MutableStateFlow<RichOngoingContentModel?>(null)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
similarity index 68%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
index 6e11977..3a7d7ba 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.domain.interactor
+package com.android.systemui.statusbar.notification.row.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
-val Kosmos.infiniteGridSizeInteractor by
- Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+fun Kosmos.getNotificationRowInteractor(repository: NotificationRowRepository) =
+ NotificationRowInteractor(repository = repository)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt
new file mode 100644
index 0000000..00f45b2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor
+
+fun Kosmos.getTimerViewModel(repository: NotificationRowRepository) =
+ TimerViewModel(
+ dumpManager = dumpManager,
+ rowInteractor = getNotificationRowInteractor(repository),
+ )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
index 1851c89..6574946 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
@@ -36,3 +36,14 @@
val Kosmos.mockSystemUIDialogFactory: SystemUIDialog.Factory by
Kosmos.Fixture { mock<SystemUIDialog.Factory>() }
+
+val Kosmos.systemUIDialogDotFactory by
+ Kosmos.Fixture {
+ SystemUIDialog.Factory(
+ applicationContext,
+ systemUIDialogManager,
+ sysUiState,
+ broadcastDispatcher,
+ dialogTransitionAnimator,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
index 141f242..83adc79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
@@ -18,9 +18,11 @@
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
import android.graphics.drawable.Drawable
import android.graphics.drawable.TestStubDrawable
import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
@@ -59,11 +61,17 @@
whenever(name).thenReturn(deviceName)
whenever(address).thenReturn(deviceAddress)
}
+ val leAudioProfile =
+ mock<LeAudioProfile> {
+ whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+ whenever(isEnabled(bluetoothDevice)).thenReturn(true)
+ }
val cachedBluetoothDevice: CachedBluetoothDevice = mock {
whenever(isHearingAidDevice).thenReturn(true)
whenever(address).thenReturn(deviceAddress)
whenever(device).thenReturn(bluetoothDevice)
whenever(name).thenReturn(deviceName)
+ whenever(profiles).thenReturn(listOf(leAudioProfile))
}
return mock<BluetoothMediaDevice> {
whenever(name).thenReturn(deviceName)
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index e8db80a..994bdb5 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -314,6 +314,10 @@
// Package: com.android.systemui
NOTE_ADAPTIVE_NOTIFICATIONS = 76;
+ // Warn the user that the device's Headless System User Mode status doesn't match the build's.
+ // Package: android
+ NOTE_WRONG_HSUM_STATUS = 77;
+
// ADD_NEW_IDS_ABOVE_THIS_LINE
// Legacy IDs with arbitrary values appear below
// Legacy IDs existed as stable non-conflicting constants prior to the O release
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 48bc803..ad216b5 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -276,3 +276,77 @@
":services.core.ravenwood.keep_all",
],
}
+
+java_library {
+ name: "services.fakes.ravenwood-jarjar",
+ installable: false,
+ srcs: [":services.fakes-sources"],
+ libs: [
+ "ravenwood-framework",
+ "services.core.ravenwood",
+ ],
+ jarjar_rules: ":ravenwood-services-jarjar-rules",
+ visibility: ["//visibility:private"],
+}
+
+java_library {
+ name: "mockito-ravenwood-prebuilt",
+ installable: false,
+ static_libs: [
+ "mockito-robolectric-prebuilt",
+ ],
+}
+
+java_library {
+ name: "inline-mockito-ravenwood-prebuilt",
+ installable: false,
+ static_libs: [
+ "inline-mockito-robolectric-prebuilt",
+ ],
+}
+
+android_ravenwood_libgroup {
+ name: "ravenwood-runtime",
+ libs: [
+ "100-framework-minus-apex.ravenwood",
+ "200-kxml2-android",
+
+ "ravenwood-runtime-common-ravenwood",
+
+ "android.test.mock.ravenwood",
+ "ravenwood-helper-runtime",
+ "hoststubgen-helper-runtime.ravenwood",
+ "services.core.ravenwood-jarjar",
+ "services.fakes.ravenwood-jarjar",
+
+ // Provide runtime versions of utils linked in below
+ "junit",
+ "truth",
+ "flag-junit",
+ "ravenwood-framework",
+ "ravenwood-junit-impl",
+ "ravenwood-junit-impl-flag",
+ "mockito-ravenwood-prebuilt",
+ "inline-mockito-ravenwood-prebuilt",
+
+ // It's a stub, so it should be towards the end.
+ "z00-all-updatable-modules-system-stubs",
+ ],
+ jni_libs: [
+ "libandroid_runtime",
+ "libravenwood_runtime",
+ ],
+}
+
+android_ravenwood_libgroup {
+ name: "ravenwood-utils",
+ libs: [
+ "junit",
+ "truth",
+ "flag-junit",
+ "ravenwood-framework",
+ "ravenwood-junit",
+ "mockito-ravenwood-prebuilt",
+ "inline-mockito-ravenwood-prebuilt",
+ ],
+}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 9353150..b4efae3 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -11,6 +11,16 @@
}
flag {
+ name: "always_allow_observing_touch_events"
+ namespace: "accessibility"
+ description: "Always allows InputFilter observing SOURCE_TOUCHSCREEN events, even if touch exploration is enabled."
+ bug: "344604959"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "resettable_dynamic_properties"
namespace: "accessibility"
description: "Maintains initial copies of a11yServiceInfo dynamic properties so they can reset on disconnect."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index 5fb60e7..f9196f3 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -1087,21 +1087,15 @@
}
}
- private boolean anyServiceWantsToObserveMotionEvent(MotionEvent event) {
- // Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
- // touch exploration.
- if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
- && (mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
- return false;
- }
- final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
- return (mCombinedGenericMotionEventSources
- & mCombinedMotionEventObservedSources
- & eventSourceWithoutClass)
- != 0;
- }
-
private boolean anyServiceWantsGenericMotionEvent(MotionEvent event) {
+ if (Flags.alwaysAllowObservingTouchEvents()) {
+ final boolean isTouchEvent = event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN);
+ if (isTouchEvent && !canShareGenericTouchEvent()) {
+ return false;
+ }
+ final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
+ return (mCombinedGenericMotionEventSources & eventSourceWithoutClass) != 0;
+ }
// Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
// touch exploration.
if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
@@ -1112,6 +1106,36 @@
return (mCombinedGenericMotionEventSources & eventSourceWithoutClass) != 0;
}
+ private boolean anyServiceWantsToObserveMotionEvent(MotionEvent event) {
+ if (Flags.alwaysAllowObservingTouchEvents()) {
+ final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
+ return (mCombinedMotionEventObservedSources & eventSourceWithoutClass) != 0;
+ }
+ // Disable SOURCE_TOUCHSCREEN generic event interception if any service is performing
+ // touch exploration.
+ if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)
+ && (mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
+ return false;
+ }
+ final int eventSourceWithoutClass = event.getSource() & ~InputDevice.SOURCE_CLASS_MASK;
+ return (mCombinedGenericMotionEventSources
+ & mCombinedMotionEventObservedSources
+ & eventSourceWithoutClass)
+ != 0;
+ }
+
+ private boolean canShareGenericTouchEvent() {
+ if ((mCombinedMotionEventObservedSources & InputDevice.SOURCE_TOUCHSCREEN) != 0) {
+ // Share touch events if a MotionEvent-observing service wants them.
+ return true;
+ }
+ if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) == 0) {
+ // Share touch events if touch exploration is not enabled.
+ return true;
+ }
+ return false;
+ }
+
public void setCombinedGenericMotionEventSources(int sources) {
mCombinedGenericMotionEventSources = sources;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4f9db8b..acd80ee 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -47,6 +47,11 @@
import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP;
import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logAccessibilityShortcutActivated;
import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -923,25 +928,11 @@
newValue, restoredFromSdk);
}
}
- case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> {
- synchronized (mLock) {
- restoreAccessibilityButtonTargetsLocked(
- previousValue, newValue);
- }
- }
- case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> {
- if (!android.view.accessibility.Flags.a11yQsShortcut()) {
- return;
- }
- restoreAccessibilityQsTargets(newValue);
- }
- case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> {
- if (!android.view.accessibility.Flags
- .restoreA11yShortcutTargetService()) {
- return;
- }
- restoreAccessibilityShortcutTargetService(previousValue, newValue);
- }
+ case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+ Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE ->
+ restoreShortcutTargets(newValue,
+ ShortcutUtils.convertToType(which));
}
}
}
@@ -1040,7 +1031,7 @@
}
persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
userState.mUserId, targetsFromSetting, str -> str);
- readAccessibilityButtonTargetsLocked(userState);
+ readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
onUserStateChangedLocked(userState);
}
@@ -1720,12 +1711,12 @@
}
// Turn on/off a11y qs shortcut for the a11y features based on the change in QS Panel
if (!a11yFeaturesToEnable.isEmpty()) {
- enableShortcutForTargets(/* enable= */ true, UserShortcutType.QUICK_SETTINGS,
+ enableShortcutForTargets(/* enable= */ true, QUICK_SETTINGS,
a11yFeaturesToEnable, userId);
}
if (!a11yFeaturesToRemove.isEmpty()) {
- enableShortcutForTargets(/* enable= */ false, UserShortcutType.QUICK_SETTINGS,
+ enableShortcutForTargets(/* enable= */ false, QUICK_SETTINGS,
a11yFeaturesToRemove, userId);
}
}
@@ -2057,100 +2048,78 @@
}
/**
- * User could enable accessibility services and configure accessibility button during the SUW.
- * Merges current value of accessibility button settings into the restored one to make sure
- * user's preferences of accessibility button updated in SUW are not lost.
- *
- * Called only during settings restore; currently supports only the owner user
- * TODO: http://b/22388012
- */
- void restoreAccessibilityButtonTargetsLocked(String oldSetting, String newSetting) {
- final Set<String> targetsFromSetting = new ArraySet<>();
- readColonDelimitedStringToSet(oldSetting, str -> str, targetsFromSetting,
- /* doMerge = */false);
- readColonDelimitedStringToSet(newSetting, str -> str, targetsFromSetting,
- /* doMerge = */true);
-
- final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
- userState.mAccessibilityButtonTargets.clear();
- userState.mAccessibilityButtonTargets.addAll(targetsFromSetting);
- persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
- UserHandle.USER_SYSTEM, userState.mAccessibilityButtonTargets, str -> str);
-
- scheduleNotifyClientsOfServicesStateChangeLocked(userState);
- onUserStateChangedLocked(userState);
- }
-
- /**
* User could configure accessibility shortcut during the SUW before restoring user data.
* Merges the current value and the new value to make sure we don't lost the setting the user's
- * preferences of accessibility qs shortcut updated in SUW are not lost.
- *
- * Called only during settings restore; currently supports only the owner user
+ * preferences of accessibility shortcut updated in SUW are not lost.
+ * Called only during settings restore; currently supports only the owner user.
+ * <P>
+ * Throws an exception if used with {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}.
+ * </P>
* TODO: http://b/22388012
*/
- private void restoreAccessibilityQsTargets(String newValue) {
+ private void restoreShortcutTargets(String newValue,
+ @UserShortcutType int shortcutType) {
+ assertNoTapShortcut(shortcutType);
+ if (shortcutType == QUICK_SETTINGS && !android.view.accessibility.Flags.a11yQsShortcut()) {
+ return;
+ }
+ if (shortcutType == HARDWARE
+ && !android.view.accessibility.Flags.restoreA11yShortcutTargetService()) {
+ return;
+ }
+
synchronized (mLock) {
final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
- final Set<String> mergedTargets = userState.getA11yQsTargets();
- readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
- /* doMerge = */ true);
+ final Set<String> mergedTargets = (shortcutType == HARDWARE)
+ ? new ArraySet<>(ShortcutUtils.getShortcutTargetsFromSettings(
+ mContext, shortcutType, userState.mUserId))
+ : userState.getShortcutTargetsLocked(shortcutType);
- userState.updateA11yQsTargetLocked(mergedTargets);
- persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+ // If dealing with the hardware shortcut,
+ // remove the default service if it wasn't present before restore,
+ // but only if the raw shortcut setting is not null (edge case during SUW).
+ // Otherwise, merge the old and new targets normally.
+ if (Flags.clearDefaultFromA11yShortcutTargetServiceRestore()
+ && shortcutType == HARDWARE) {
+ final String defaultService =
+ mContext.getString(R.string.config_defaultAccessibilityService);
+ final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
+ ? null : ComponentName.unflattenFromString(defaultService);
+ boolean shouldClearDefaultService = defaultServiceComponent != null
+ && !stringSetContainsComponentName(mergedTargets, defaultServiceComponent);
+ readColonDelimitedStringToSet(newValue, str -> str,
+ mergedTargets, /*doMerge=*/true);
+
+ if (shouldClearDefaultService && stringSetContainsComponentName(
+ mergedTargets, defaultServiceComponent)) {
+ Slog.i(LOG_TAG, "Removing default service " + defaultService
+ + " from restore of "
+ + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+ mergedTargets.removeIf(str ->
+ defaultServiceComponent.equals(ComponentName.unflattenFromString(str)));
+ }
+ if (mergedTargets.isEmpty()) {
+ return;
+ }
+ } else {
+ readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
+ /* doMerge = */ true);
+ }
+
+ userState.updateShortcutTargetsLocked(mergedTargets, shortcutType);
+ persistColonDelimitedSetToSettingLocked(ShortcutUtils.convertToKey(shortcutType),
UserHandle.USER_SYSTEM, mergedTargets, str -> str);
scheduleNotifyClientsOfServicesStateChangeLocked(userState);
onUserStateChangedLocked(userState);
}
}
- /**
- * Merges the old and restored value of
- * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE}.
- *
- * <p>Also clears out {@link R.string#config_defaultAccessibilityService} from
- * the merged set if it was not present before restoring.
- */
- private void restoreAccessibilityShortcutTargetService(
- String oldValue, String restoredValue) {
- final Set<String> targetsFromSetting = new ArraySet<>();
- readColonDelimitedStringToSet(oldValue, str -> str,
- targetsFromSetting, /*doMerge=*/false);
- final String defaultService =
- mContext.getString(R.string.config_defaultAccessibilityService);
- final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
- ? null : ComponentName.unflattenFromString(defaultService);
- boolean shouldClearDefaultService = defaultServiceComponent != null
- && !stringSetContainsComponentName(targetsFromSetting, defaultServiceComponent);
- readColonDelimitedStringToSet(restoredValue, str -> str,
- targetsFromSetting, /*doMerge=*/true);
- if (Flags.clearDefaultFromA11yShortcutTargetServiceRestore()) {
- if (shouldClearDefaultService && stringSetContainsComponentName(
- targetsFromSetting, defaultServiceComponent)) {
- Slog.i(LOG_TAG, "Removing default service " + defaultService
- + " from restore of "
- + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
- targetsFromSetting.removeIf(str ->
- defaultServiceComponent.equals(ComponentName.unflattenFromString(str)));
- }
- if (targetsFromSetting.isEmpty()) {
- return;
- }
- }
- synchronized (mLock) {
- final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
- final Set<String> shortcutTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
- shortcutTargets.clear();
- shortcutTargets.addAll(targetsFromSetting);
- persistColonDelimitedSetToSettingLocked(
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
- UserHandle.USER_SYSTEM, targetsFromSetting, str -> str);
- scheduleNotifyClientsOfServicesStateChangeLocked(userState);
- onUserStateChangedLocked(userState);
- }
+ private String getRawShortcutSetting(int userId, @UserShortcutType int shortcutType) {
+ return Settings.Secure.getStringForUser(mContext.getContentResolver(),
+ ShortcutUtils.convertToKey(shortcutType), userId);
}
+
/**
* Returns {@code true} if the set contains the provided non-null {@link ComponentName}.
*
@@ -2263,7 +2232,7 @@
private void showAccessibilityTargetsSelection(int displayId,
@UserShortcutType int shortcutType) {
final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
- final String chooserClassName = (shortcutType == UserShortcutType.HARDWARE)
+ final String chooserClassName = (shortcutType == HARDWARE)
? AccessibilityShortcutChooserActivity.class.getName()
: AccessibilityButtonChooserActivity.class.getName();
intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
@@ -3236,9 +3205,9 @@
somethingChanged |= readAudioDescriptionEnabledSettingLocked(userState);
somethingChanged |= readMagnificationEnabledSettingsLocked(userState);
somethingChanged |= readAutoclickEnabledSettingLocked(userState);
- somethingChanged |= readAccessibilityShortcutKeySettingLocked(userState);
- somethingChanged |= readAccessibilityQsTargetsLocked(userState);
- somethingChanged |= readAccessibilityButtonTargetsLocked(userState);
+ somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, HARDWARE);
+ somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
+ somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState);
somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
@@ -3386,60 +3355,34 @@
userState.setSendMotionEventsEnabled(sendMotionEvents);
}
- private boolean readAccessibilityShortcutKeySettingLocked(AccessibilityUserState userState) {
- final String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userState.mUserId);
+ /**
+ * Throws an exception for {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP} types.
+ */
+ private boolean readAccessibilityShortcutTargetsLocked(AccessibilityUserState userState,
+ @UserShortcutType int shortcutType) {
+ assertNoTapShortcut(shortcutType);
+ final String settingValue = getRawShortcutSetting(userState.mUserId, shortcutType);
final Set<String> targetsFromSetting = new ArraySet<>();
- readColonDelimitedStringToSet(settingValue, str -> str, targetsFromSetting, false);
- // Fall back to device's default a11y service, only when setting is never updated.
- if (settingValue == null) {
+ // If dealing with an empty hardware shortcut, fall back to the default value.
+ if (shortcutType == HARDWARE && settingValue == null) {
final String defaultService = mContext.getString(
R.string.config_defaultAccessibilityService);
if (!TextUtils.isEmpty(defaultService)) {
- targetsFromSetting.add(defaultService);
+ // Convert to component name to reformat the target if it has a relative path.
+ ComponentName name = ComponentName.unflattenFromString(defaultService);
+ if (name != null) {
+ targetsFromSetting.add(name.flattenToString());
+ }
}
+ } else {
+ readColonDelimitedStringToSet(settingValue, str -> str, targetsFromSetting, false);
}
- final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
- if (targetsFromSetting.equals(currentTargets)) {
- return false;
+ if (userState.updateShortcutTargetsLocked(targetsFromSetting, shortcutType)) {
+ scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+ return true;
}
- currentTargets.clear();
- currentTargets.addAll(targetsFromSetting);
- scheduleNotifyClientsOfServicesStateChangeLocked(userState);
- return true;
- }
-
- private boolean readAccessibilityQsTargetsLocked(AccessibilityUserState userState) {
- final Set<String> targetsFromSetting = new ArraySet<>();
- readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
- userState.mUserId, str -> str, targetsFromSetting);
-
- final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
- if (targetsFromSetting.equals(currentTargets)) {
- return false;
- }
- userState.updateA11yQsTargetLocked(targetsFromSetting);
- scheduleNotifyClientsOfServicesStateChangeLocked(userState);
- return true;
- }
-
- private boolean readAccessibilityButtonTargetsLocked(AccessibilityUserState userState) {
- final Set<String> targetsFromSetting = new ArraySet<>();
- readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
- userState.mUserId, str -> str, targetsFromSetting);
-
- final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
- if (targetsFromSetting.equals(currentTargets)) {
- return false;
- }
- currentTargets.clear();
- currentTargets.addAll(targetsFromSetting);
- scheduleNotifyClientsOfServicesStateChangeLocked(userState);
- return true;
+ return false;
}
private boolean readAccessibilityButtonTargetComponentLocked(AccessibilityUserState userState) {
@@ -3487,14 +3430,10 @@
*/
private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) {
final Set<String> currentTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
- final int lastSize = currentTargets.size();
- if (lastSize == 0) {
- return;
- }
+ userState.getShortcutTargetsLocked(HARDWARE);
currentTargets.removeIf(
name -> !userState.isShortcutTargetInstalledLocked(name));
- if (lastSize == currentTargets.size()) {
+ if (!userState.updateShortcutTargetsLocked(currentTargets, HARDWARE)) {
return;
}
@@ -3680,13 +3619,9 @@
final Set<String> currentTargets =
userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
- final int lastSize = currentTargets.size();
- if (lastSize == 0) {
- return;
- }
currentTargets.removeIf(
name -> !userState.isShortcutTargetInstalledLocked(name));
- if (lastSize == currentTargets.size()) {
+ if (!userState.updateShortcutTargetsLocked(currentTargets, SOFTWARE)) {
return;
}
@@ -3719,8 +3654,7 @@
return;
}
final Set<String> buttonTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
- int lastSize = buttonTargets.size();
+ userState.getShortcutTargetsLocked(SOFTWARE);
buttonTargets.removeIf(name -> {
if (packageName != null && name != null && !name.contains(packageName)) {
return false;
@@ -3752,13 +3686,11 @@
}
return false;
});
- boolean changed = (lastSize != buttonTargets.size());
- lastSize = buttonTargets.size();
final Set<String> shortcutKeyTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
+ userState.getShortcutTargetsLocked(HARDWARE);
final Set<String> qsShortcutTargets =
- userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
+ userState.getShortcutTargetsLocked(QUICK_SETTINGS);
userState.mEnabledServices.forEach(componentName -> {
if (packageName != null && componentName != null
&& !packageName.equals(componentName.getPackageName())) {
@@ -3790,8 +3722,7 @@
+ " should be assign to the button or shortcut.");
buttonTargets.add(serviceName);
});
- changed |= (lastSize != buttonTargets.size());
- if (!changed) {
+ if (!userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE)) {
return;
}
@@ -3815,10 +3746,10 @@
}
final Set<String> targets =
- userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
+ userState.getShortcutTargetsLocked(QUICK_SETTINGS);
// Removes the targets that are no longer installed on the device.
- boolean somethingChanged = targets.removeIf(
+ targets.removeIf(
name -> !userState.isShortcutTargetInstalledLocked(name));
// Add the target if the a11y service is enabled and the tile exist in QS panel
Set<ComponentName> enabledServices = userState.getEnabledServicesLocked();
@@ -3829,14 +3760,13 @@
ComponentName tileService =
a11yFeatureToTileService.getOrDefault(enabledService, null);
if (tileService != null && currentA11yTilesInQsPanel.contains(tileService)) {
- somethingChanged |= targets.add(enabledService.flattenToString());
+ targets.add(enabledService.flattenToString());
}
}
- if (!somethingChanged) {
+ if (!userState.updateShortcutTargetsLocked(targets, QUICK_SETTINGS)) {
return;
}
- userState.updateA11yQsTargetLocked(targets);
// Update setting key with new value.
persistColonDelimitedSetToSettingLocked(
@@ -3862,14 +3792,14 @@
final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = new ArrayList<>(3);
shortcutTypeAndShortcutSetting.add(
- new Pair<>(UserShortcutType.HARDWARE,
+ new Pair<>(HARDWARE,
Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE));
shortcutTypeAndShortcutSetting.add(
new Pair<>(UserShortcutType.SOFTWARE,
Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS));
if (android.view.accessibility.Flags.a11yQsShortcut()) {
shortcutTypeAndShortcutSetting.add(
- new Pair<>(UserShortcutType.QUICK_SETTINGS,
+ new Pair<>(QUICK_SETTINGS,
Settings.Secure.ACCESSIBILITY_QS_TARGETS));
}
@@ -3883,7 +3813,7 @@
shortcutSettingName,
userState.mUserId, currentTargets, str -> str);
- if (shortcutType != UserShortcutType.QUICK_SETTINGS) {
+ if (shortcutType != QUICK_SETTINGS) {
continue;
}
@@ -3968,7 +3898,7 @@
mMainHandler.sendMessage(obtainMessage(
AccessibilityManagerService::performAccessibilityShortcutInternal, this,
- Display.DEFAULT_DISPLAY, UserShortcutType.HARDWARE, targetName));
+ Display.DEFAULT_DISPLAY, HARDWARE, targetName));
}
/**
@@ -4115,7 +4045,7 @@
final boolean requestA11yButton = (installedServiceInfo.flags
& FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
// Turns on / off the accessibility service
- if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == UserShortcutType.HARDWARE)
+ if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == HARDWARE)
|| (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) {
if (serviceConnection == null) {
logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType,
@@ -4129,7 +4059,7 @@
}
return true;
}
- if (shortcutType == UserShortcutType.HARDWARE && targetSdk > Build.VERSION_CODES.Q
+ if (shortcutType == HARDWARE && targetSdk > Build.VERSION_CODES.Q
&& requestA11yButton) {
if (!userState.getEnabledServicesLocked().contains(assignedTarget)) {
enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId);
@@ -4222,7 +4152,7 @@
validNewTargets = newTargets;
// filter out targets that doesn't have qs shortcut
- if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+ if (shortcutType == QUICK_SETTINGS) {
validNewTargets = newTargets.stream().filter(target -> {
ComponentName targetComponent = ComponentName.unflattenFromString(target);
return featureToTileMap.containsKey(targetComponent);
@@ -4240,10 +4170,10 @@
/* defaultEmptyString= */ ""
);
- if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+ if (shortcutType == QUICK_SETTINGS) {
int numOfFeatureChanged = Math.abs(currentTargets.size() - validNewTargets.size());
logMetricForQsShortcutConfiguration(enable, numOfFeatureChanged);
- userState.updateA11yQsTargetLocked(validNewTargets);
+ userState.updateShortcutTargetsLocked(validNewTargets, QUICK_SETTINGS);
scheduleNotifyClientsOfServicesStateChangeLocked(userState);
onUserStateChangedLocked(userState);
}
@@ -4257,7 +4187,7 @@
}
// Add or Remove tile in QS Panel
- if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+ if (shortcutType == QUICK_SETTINGS) {
mMainHandler.sendMessage(obtainMessage(
AccessibilityManagerService::updateA11yTileServicesInQuickSettingsPanel,
this, validNewTargets, currentTargets, userId));
@@ -4266,7 +4196,7 @@
if (!enable) {
return;
}
- if (shortcutType == UserShortcutType.HARDWARE) {
+ if (shortcutType == HARDWARE) {
skipVolumeShortcutDialogTimeoutRestriction(userId);
if (com.android.server.accessibility.Flags.enableHardwareShortcutDisablesWarning()) {
persistIntToSetting(
@@ -4461,6 +4391,7 @@
shortcutTargets.add(serviceName);
}
}
+ userState.updateShortcutTargetsLocked(Set.copyOf(shortcutTargets), shortcutType);
return shortcutTargets;
}
}
@@ -5672,7 +5603,7 @@
|| mShowImeWithHardKeyboardUri.equals(uri)) {
userState.reconcileSoftKeyboardModeWithSettingsLocked();
} else if (mAccessibilityShortcutServiceIdUri.equals(uri)) {
- if (readAccessibilityShortcutKeySettingLocked(userState)) {
+ if (readAccessibilityShortcutTargetsLocked(userState, HARDWARE)) {
onUserStateChangedLocked(userState);
}
} else if (mAccessibilityButtonComponentIdUri.equals(uri)) {
@@ -5680,7 +5611,7 @@
onUserStateChangedLocked(userState);
}
} else if (mAccessibilityButtonTargetsUri.equals(uri)) {
- if (readAccessibilityButtonTargetsLocked(userState)) {
+ if (readAccessibilityShortcutTargetsLocked(userState, SOFTWARE)) {
onUserStateChangedLocked(userState);
}
} else if (mUserNonInteractiveUiTimeoutUri.equals(uri)
@@ -6505,4 +6436,10 @@
String metricId = enable ? METRIC_ID_QS_SHORTCUT_ADD : METRIC_ID_QS_SHORTCUT_REMOVE;
Counter.logIncrementWithUid(metricId, Binder.getCallingUid(), numOfFeatures);
}
+
+ private void assertNoTapShortcut(@UserShortcutType int shortcutType) {
+ if ((shortcutType & (TRIPLETAP | TWOFINGER_DOUBLETAP)) != 0) {
+ throw new IllegalArgumentException("Tap shortcuts are not supported.");
+ }
+ }
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index a37a184..de1c86a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -777,12 +777,15 @@
* @return The array set of the strings
*/
public ArraySet<String> getShortcutTargetsLocked(@UserShortcutType int shortcutType) {
+ return new ArraySet<>(getShortcutTargetsInternalLocked(shortcutType));
+ }
+ private ArraySet<String> getShortcutTargetsInternalLocked(@UserShortcutType int shortcutType) {
if (shortcutType == UserShortcutType.HARDWARE) {
return mAccessibilityShortcutKeyTargets;
} else if (shortcutType == UserShortcutType.SOFTWARE) {
return mAccessibilityButtonTargets;
} else if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
- return getA11yQsTargets();
+ return mAccessibilityQsTargets;
} else if ((shortcutType == UserShortcutType.TRIPLETAP
&& isMagnificationSingleFingerTripleTapEnabledLocked()) || (
shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP
@@ -795,6 +798,32 @@
}
/**
+ * Updates the corresponding shortcut targets with the provided set.
+ * Tap shortcuts don't operate using sets of targets,
+ * so trying to update {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}
+ * will instead throw an {@code IllegalArgumentException}
+ * @param newTargets set of targets to replace the existing set.
+ * @param shortcutType type to be replaced.
+ * @return {@code true} if the set was changed, or {@code false} if the elements are the same.
+ * @throws IllegalArgumentException if {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP} is used.
+ */
+ boolean updateShortcutTargetsLocked(
+ Set<String> newTargets, @UserShortcutType int shortcutType) {
+ final int mask = UserShortcutType.TRIPLETAP | UserShortcutType.TWOFINGER_DOUBLETAP;
+ if ((shortcutType & mask) != 0) {
+ throw new IllegalArgumentException("Tap shortcuts cannot be updated with target sets.");
+ }
+
+ final Set<String> currentTargets = getShortcutTargetsInternalLocked(shortcutType);
+ if (newTargets.equals(currentTargets)) {
+ return false;
+ }
+ currentTargets.clear();
+ currentTargets.addAll(newTargets);
+ return true;
+ }
+
+ /**
* Whether or not the given shortcut target is installed in device.
*
* @param name The shortcut target name
@@ -844,8 +873,9 @@
);
}
- Set<String> targets = getShortcutTargetsLocked(shortcutType);
- boolean result = targets.removeIf(name -> {
+ // getting internal set lets us directly modify targets, as it's not a copy.
+ Set<String> targets = getShortcutTargetsInternalLocked(shortcutType);
+ return targets.removeIf(name -> {
ComponentName componentName;
if (name == null
|| (componentName = ComponentName.unflattenFromString(name)) == null) {
@@ -853,11 +883,6 @@
}
return componentName.equals(target);
});
- if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
- updateA11yQsTargetLocked(targets);
- }
-
- return result;
}
/**
@@ -1114,11 +1139,6 @@
);
}
- public void updateA11yQsTargetLocked(Set<String> targets) {
- mAccessibilityQsTargets.clear();
- mAccessibilityQsTargets.addAll(targets);
- }
-
/**
* Returns a copy of the targets which has qs shortcut turned on
*/
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index b64aa8a..ea6351b 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -364,7 +364,7 @@
}
@RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
- private int invokeContextualSearchIntent(Intent launchIntent) {
+ private int invokeContextualSearchIntent(Intent launchIntent, final int userId) {
// Contextual search starts with a frozen screen - so we launch without
// any system animations or starting window.
final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext,
@@ -372,7 +372,7 @@
opts.setDisableStartingWindow(true);
return mAtmInternal.startActivityWithScreenshot(launchIntent,
mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null,
- opts.toBundle(), Binder.getCallingUserHandle().getIdentifier());
+ opts.toBundle(), userId);
}
private void enforcePermission(@NonNull final String func) {
@@ -446,6 +446,8 @@
synchronized (this) {
if (DEBUG_USER) Log.d(TAG, "startContextualSearch");
enforcePermission("startContextualSearch");
+ final int callingUserId = Binder.getCallingUserHandle().getIdentifier();
+
mAssistDataRequester.cancel();
// Creates a new CallbackToken at mToken and an expiration handler.
issueToken();
@@ -455,7 +457,7 @@
Binder.withCleanCallingIdentity(() -> {
Intent launchIntent = getContextualSearchIntent(entrypoint, mToken);
if (launchIntent != null) {
- int result = invokeContextualSearchIntent(launchIntent);
+ int result = invokeContextualSearchIntent(launchIntent, callingUserId);
if (DEBUG_USER) Log.d(TAG, "Launch result: " + result);
}
});
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 44aea15..e2ab0d9 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -1603,7 +1603,7 @@
} else {
mPackageToSharedUidAllowList.put(pkgName, sharedUid);
}
- }
+ } break;
case "asl-file": {
String packageName = parser.getAttributeValue(null, "package");
String path = parser.getAttributeValue(null, "path");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 195e94b..df35ff3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -730,7 +730,7 @@
/** Whether some specified important processes are allowed to use FIFO priority. */
boolean mAllowSpecifiedFifoScheduling = true;
- @GuardedBy("this")
+ @GuardedBy("mStrictModeCallbacks")
private final SparseArray<IUnsafeIntentStrictModeCallback>
mStrictModeCallbacks = new SparseArray<>();
@@ -9535,18 +9535,20 @@
* @param callback The binder used to communicate the violations.
*/
@Override
- public synchronized void registerStrictModeCallback(IBinder callback) {
- int callingPid = Binder.getCallingPid();
- mStrictModeCallbacks.put(callingPid,
- IUnsafeIntentStrictModeCallback.Stub.asInterface(callback));
- try {
- callback.linkToDeath(() -> {
- synchronized (ActivityManagerService.this) {
- mStrictModeCallbacks.remove(callingPid);
- }
- }, 0);
- } catch (RemoteException e) {
- mStrictModeCallbacks.remove(callingPid);
+ public void registerStrictModeCallback(IBinder callback) {
+ final int callingPid = Binder.getCallingPid();
+ synchronized (mStrictModeCallbacks) {
+ mStrictModeCallbacks.put(callingPid,
+ IUnsafeIntentStrictModeCallback.Stub.asInterface(callback));
+ try {
+ callback.linkToDeath(() -> {
+ synchronized (mStrictModeCallbacks) {
+ mStrictModeCallbacks.remove(callingPid);
+ }
+ }, 0);
+ } catch (RemoteException e) {
+ mStrictModeCallbacks.remove(callingPid);
+ }
}
}
@@ -10013,8 +10015,9 @@
if (crashInfo != null && crashInfo.stackTrace != null) {
sb.append(crashInfo.stackTrace);
}
- boolean shouldAddLogs = logcatLines > 0 || kernelLogLines > 0;
- if (!runSynchronously && shouldAddLogs) {
+ boolean shouldAddLogs = (logcatLines > 0 || kernelLogLines > 0)
+ && (Flags.collectLogcatOnRunSynchronously() || !runSynchronously);
+ if (shouldAddLogs) {
sb.append("\n");
if (logcatLines > 0) {
fetchLogcatBuffers(sb, logcatLines, LOGCAT_TIMEOUT_SEC,
@@ -19907,7 +19910,7 @@
public void triggerUnsafeIntentStrictMode(int callingPid, int type, Intent intent) {
final IUnsafeIntentStrictModeCallback callback;
final Intent i = intent.cloneFilter();
- synchronized (ActivityManagerService.this) {
+ synchronized (mStrictModeCallbacks) {
callback = mStrictModeCallbacks.get(callingPid);
}
if (callback != null) {
@@ -19915,7 +19918,7 @@
try {
callback.onUnsafeIntent(type, i);
} catch (RemoteException e) {
- synchronized (ActivityManagerService.this) {
+ synchronized (mStrictModeCallbacks) {
mStrictModeCallbacks.remove(callingPid);
}
}
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index bb52857..a30590f 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -152,3 +152,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "collect_logcat_on_run_synchronously"
+ namespace: "dropbox"
+ description: "Allow logcat collection on synchronous dropbox collection"
+ bug: "324222683"
+ is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index bce1830..27fda15 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -414,7 +414,8 @@
}
if (!mScoManagedByAudio) {
boolean isBtScoRequested = isBluetoothScoRequested();
- if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
+ if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive()
+ || !mBtHelper.isBluetoothScoRequestedInternally())) {
if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
+ uid);
@@ -1148,13 +1149,14 @@
}
/*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
- if (AudioService.DEBUG_COMM_RTE) {
- Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource);
- }
synchronized (mBluetoothAudioStateLock) {
+ boolean isBtScoRequested = isBluetoothScoRequested();
+ Log.i(TAG, "setBluetoothScoOn: " + on + ", mBluetoothScoOn: "
+ + mBluetoothScoOn + ", isBtScoRequested: " + isBtScoRequested
+ + ", from: " + eventSource);
mBluetoothScoOn = on;
updateAudioHalBluetoothState();
- postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource);
+ postUpdateCommunicationRouteClient(isBtScoRequested, eventSource);
}
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6d1983e..c89992d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -11698,7 +11698,7 @@
static final int LOG_NB_EVENTS_VOLUME = 100;
static final int LOG_NB_EVENTS_DYN_POLICY = 10;
static final int LOG_NB_EVENTS_SPATIAL = 30;
- static final int LOG_NB_EVENTS_SOUND_DOSE = 30;
+ static final int LOG_NB_EVENTS_SOUND_DOSE = 50;
static final int LOG_NB_EVENTS_LOUDNESS_CODEC = 30;
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 749044e..8ea28be 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -577,6 +577,7 @@
static final int DOSE_REPEAT_5X = 2;
static final int DOSE_ACCUMULATION_START = 3;
static final int LOWER_VOLUME_TO_RS1 = 4;
+ static final int UPDATE_ABS_VOLUME_ATTENUATION = 5;
final int mEventType;
final float mFloatValue;
@@ -608,6 +609,10 @@
return new SoundDoseEvent(LOWER_VOLUME_TO_RS1, 0 /*ignored*/, 0 /*ignored*/);
}
+ static SoundDoseEvent getAbsVolumeAttenuationEvent(float attenuation, int device) {
+ return new SoundDoseEvent(UPDATE_ABS_VOLUME_ATTENUATION, attenuation, device);
+ }
+
@Override
public String eventToString() {
switch (mEventType) {
@@ -623,6 +628,10 @@
return "CSD accumulating: RS2 entered";
case LOWER_VOLUME_TO_RS1:
return "CSD lowering volume to RS1";
+ case UPDATE_ABS_VOLUME_ATTENUATION:
+ return String.format(java.util.Locale.US,
+ "Updating CSD absolute volume attenuation on device %d with %.2f dB ",
+ mLongValue, mFloatValue);
}
return new StringBuilder("FIXME invalid event type:").append(mEventType).toString();
}
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 991f94b..8008717 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -378,7 +378,6 @@
/*package*/ synchronized void onReceiveBtEvent(Intent intent) {
final String action = intent.getAction();
- Log.i(TAG, "onReceiveBtEvent action: " + action + " mScoAudioState: " + mScoAudioState);
if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE,
android.bluetooth.BluetoothDevice.class);
@@ -405,6 +404,7 @@
private void onScoAudioStateChanged(int state) {
boolean broadcast = false;
int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
+ Log.i(TAG, "onScoAudioStateChanged state: " + state + " mScoAudioState: " + mScoAudioState);
if (mDeviceBroker.isScoManagedByAudio()) {
switch (state) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
@@ -488,6 +488,11 @@
== BluetoothHeadset.STATE_AUDIO_CONNECTED;
}
+ /*package*/ synchronized boolean isBluetoothScoRequestedInternally() {
+ return mScoAudioState == SCO_STATE_ACTIVE_INTERNAL
+ || mScoAudioState == SCO_STATE_ACTIVATE_REQ;
+ }
+
// @GuardedBy("mDeviceBroker.mSetModeLock")
@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
/*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index e28ae95..5c74304 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -39,6 +39,7 @@
import android.media.ISoundDose;
import android.media.ISoundDoseCallback;
import android.media.SoundDoseRecord;
+import android.media.VolumeInfo;
import android.os.Binder;
import android.os.Message;
import android.os.RemoteException;
@@ -895,6 +896,8 @@
try {
if (!isAbsoluteVolume) {
+ mLogger.enqueue(
+ SoundDoseEvent.getAbsVolumeAttenuationEvent(/*attenuation=*/0.f, device));
// remove any possible previous attenuation
soundDose.updateAttenuation(/* attenuationDB= */0.f, device);
@@ -903,10 +906,11 @@
if (AudioService.mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC
&& safeDevicesContains(device)) {
- soundDose.updateAttenuation(
- -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
- (newIndex + 5) / 10,
- device), device);
+ float attenuationDb = -AudioSystem.getStreamVolumeDB(AudioSystem.STREAM_MUSIC,
+ (newIndex + 5) / 10, device);
+ mLogger.enqueue(
+ SoundDoseEvent.getAbsVolumeAttenuationEvent(attenuationDb, device));
+ soundDose.updateAttenuation(attenuationDb, device);
}
} catch (RemoteException e) {
Log.e(TAG, "Could not apply the attenuation for MEL calculation with volume index "
@@ -1313,22 +1317,30 @@
/** Called when handling MSG_LOWER_VOLUME_TO_RS1 */
private void onLowerVolumeToRs1() {
- mLogger.enqueue(SoundDoseEvent.getLowerVolumeToRs1Event());
final ArrayList<AudioDeviceAttributes> devices = mAudioService.getDevicesForAttributesInt(
- new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(), true);
- final int nativeDeviceType;
- final AudioDeviceAttributes ada;
- if (!devices.isEmpty()) {
- ada = devices.get(0);
- nativeDeviceType = ada.getInternalType();
- } else {
- nativeDeviceType = AudioSystem.DEVICE_OUT_USB_HEADSET;
- ada = new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_USB_HEADSET, "");
+ new AudioAttributes.Builder().setUsage(
+ AudioAttributes.USAGE_MEDIA).build(), /*forVolume=*/true);
+ if (devices.isEmpty()) {
+ Log.e(TAG, "Cannot lower the volume to RS1, no devices registered for USAGE_MEDIA");
+ return;
}
- final int index = safeMediaVolumeIndex(nativeDeviceType);
- mAudioService.setStreamVolumeWithAttributionInt(STREAM_MUSIC, index / 10, /*flags*/ 0, ada,
- mContext.getOpPackageName(), /*attributionTag=*/null,
- true /*canChangeMuteAndUpdateController*/);
+ final AudioDeviceAttributes ada = devices.get(0);
+ final int nativeDeviceType = ada.getInternalType();
+ final int index = safeMediaVolumeIndex(nativeDeviceType) / 10;
+ final VolumeInfo curVolume = mAudioService.getDeviceVolume(
+ new VolumeInfo.Builder(STREAM_MUSIC).build(), ada,
+ /*callingPackage=*/"sounddosehelper");
+
+ if (index < curVolume.getVolumeIndex()) {
+ mLogger.enqueue(SoundDoseEvent.getLowerVolumeToRs1Event());
+ mAudioService.setStreamVolumeWithAttributionInt(STREAM_MUSIC, index, /*flags*/ 0, ada,
+ mContext.getOpPackageName(), /*attributionTag=*/null,
+ /*canChangeMuteAndUpdateController=*/true);
+ } else {
+ Log.i(TAG, "The current volume " + curVolume.getVolumeIndex()
+ + " for device type " + nativeDeviceType
+ + " is already smaller or equal to the safe index volume " + index);
+ }
}
// StreamVolumeCommand contains the information needed to defer the process of
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 0fef55d..a188e79 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -84,6 +84,14 @@
}
@Override
+ public void cancelBlockScreenOn() {
+ if (mDisplayOffloader == null) {
+ return;
+ }
+ mDisplayOffloader.cancelBlockScreenOn();
+ }
+
+ @Override
public float[] getAutoBrightnessLevels(int mode) {
if (mode < 0 || mode > AUTO_BRIGHTNESS_MODE_MAX) {
throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d14f2a0..c298bbf 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2110,6 +2110,17 @@
Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
}
+ private void cancelUnblockScreenOnByDisplayOffload() {
+ if (mDisplayOffloadSession == null) {
+ return;
+ }
+ if (mPendingScreenOnUnblockerByDisplayOffload != null) {
+ // Already unblocked.
+ return;
+ }
+ mDisplayOffloadSession.cancelBlockScreenOn();
+ }
+
private boolean setScreenState(int state, @Display.StateReason int reason) {
return setScreenState(state, reason, false /*reportOnly*/);
}
@@ -2126,6 +2137,9 @@
blockScreenOnByDisplayOffload(mDisplayOffloadSession);
} else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) {
// No longer turning screen on, so unblock previous screen on blocking immediately.
+ if (mFlags.isOffloadSessionCancelBlockScreenOnEnabled()) {
+ cancelUnblockScreenOnByDisplayOffload();
+ }
unblockScreenOnByDisplayOffload();
mScreenTurningOnWasBlockedByDisplayOffload = false;
}
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index b43b35b..ddb091d 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -15,6 +15,8 @@
*/
package com.android.server.display.brightness.strategy;
+import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
@@ -133,14 +135,20 @@
// We are still in the process of updating the power state, so there's no need to trigger
// an update again
switchMode(targetDisplayState, /* sendUpdate= */ false);
- final boolean autoBrightnessEnabledInDoze =
- allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
+
+ // If the policy is POLICY_DOZE and the display state is STATE_ON, auto-brightness should
+ // only be enabled if the config allows it
+ final boolean autoBrightnessEnabledInDoze = allowAutoBrightnessWhileDozingConfig
+ && policy == POLICY_DOZE && targetDisplayState != Display.STATE_OFF;
+
mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
- && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
+ && ((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+ || autoBrightnessEnabledInDoze)
&& brightnessReason != BrightnessReason.REASON_OVERRIDE
&& mAutomaticBrightnessController != null;
mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
- && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
+ && !((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+ || autoBrightnessEnabledInDoze);
final int autoBrightnessState = mIsAutoBrightnessEnabled
&& brightnessReason != BrightnessReason.REASON_FOLLOWER
? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
index 4d9c18a..c87872c 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
@@ -15,6 +15,8 @@
*/
package com.android.server.display.brightness.strategy;
+import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.display.BrightnessConfiguration;
@@ -107,14 +109,19 @@
public void setAutoBrightnessState(int targetDisplayState,
boolean allowAutoBrightnessWhileDozingConfig, int brightnessReason, int policy,
float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) {
- final boolean autoBrightnessEnabledInDoze =
- allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
+ // If the policy is POLICY_DOZE and the display state is STATE_ON, auto-brightness should
+ // only be enabled if the config allows it
+ final boolean autoBrightnessEnabledInDoze = allowAutoBrightnessWhileDozingConfig
+ && policy == POLICY_DOZE && targetDisplayState != Display.STATE_OFF;
+
mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
- && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
+ && ((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+ || autoBrightnessEnabledInDoze)
&& brightnessReason != BrightnessReason.REASON_OVERRIDE
&& mAutomaticBrightnessController != null;
mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
- && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
+ && !((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+ || autoBrightnessEnabledInDoze);
final int autoBrightnessState = mIsAutoBrightnessEnabled
&& brightnessReason != BrightnessReason.REASON_FOLLOWER
? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index f56d803..41d18cd 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -179,6 +179,11 @@
Flags::offloadDozeOverrideHoldsWakelock
);
+ private final FlagState mOffloadSessionCancelBlockScreenOn =
+ new FlagState(
+ Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON,
+ Flags::offloadSessionCancelBlockScreenOn);
+
/**
* @return {@code true} if 'port' is allowed in display layout configuration file.
*/
@@ -352,6 +357,10 @@
return mOffloadDozeOverrideHoldsWakelock.isEnabled();
}
+ public boolean isOffloadSessionCancelBlockScreenOnEnabled() {
+ return mOffloadSessionCancelBlockScreenOn.isEnabled();
+ }
+
/**
* @return Whether to ignore preferredRefreshRate app request conversion to display mode or not
*/
@@ -399,6 +408,7 @@
pw.println(" " + mIgnoreAppPreferredRefreshRate);
pw.println(" " + mSynthetic60hzModes);
pw.println(" " + mOffloadDozeOverrideHoldsWakelock);
+ pw.println(" " + mOffloadSessionCancelBlockScreenOn);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 95d0ca3..1ea5c0b 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -299,3 +299,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "offload_session_cancel_block_screen_on"
+ namespace: "wear_frameworks"
+ description: "Flag for DisplayPowerController to start notifying DisplayOffloadSession about cancelling screen on blocker."
+ bug: "331725519"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index 7d48527..b77f47d 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -69,7 +69,6 @@
@NonNull
private final ImeTargetVisibilityPolicy mImeTargetVisibilityPolicy;
-
DefaultImeVisibilityApplier(InputMethodManagerService service) {
mService = service;
mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
@@ -80,8 +79,9 @@
@Override
public void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
@InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {
- final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+ @SoftInputShowHideReason int reason, @UserIdInt int userId) {
+ final var bindingController = mService.getInputMethodBindingController(userId);
+ final IInputMethodInvoker curMethod = bindingController.getCurMethod();
if (curMethod != null) {
if (DEBUG) {
Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken
@@ -99,7 +99,7 @@
mService.mImeBindingState.mFocusedWindowSoftInputMode));
}
mService.onShowHideSoftInputRequested(true /* show */, showInputToken, reason,
- statsToken);
+ statsToken, userId);
}
}
}
@@ -107,8 +107,10 @@
@GuardedBy("ImfLock.class")
@Override
public void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
- final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
+ @UserIdInt int userId) {
+ final var bindingController = mService.getInputMethodBindingController(userId);
+ final IInputMethodInvoker curMethod = bindingController.getCurMethod();
if (curMethod != null) {
// The IME will report its visible state again after the following message finally
// delivered to the IME process as an IPC. Hence the inconsistency between
@@ -130,7 +132,7 @@
mService.mImeBindingState.mFocusedWindowSoftInputMode));
}
mService.onShowHideSoftInputRequested(false /* show */, hideInputToken, reason,
- statsToken);
+ statsToken, userId);
}
}
}
@@ -180,7 +182,7 @@
setImeVisibilityOnFocusedWindowClient(false);
} else {
mService.hideCurrentInputLocked(windowToken, statsToken,
- 0 /* flags */, null /* resultReceiver */, reason);
+ 0 /* flags */, null /* resultReceiver */, reason);
}
break;
case STATE_HIDE_IME_NOT_ALWAYS:
@@ -199,14 +201,14 @@
} else {
mService.showCurrentInputLocked(windowToken, statsToken,
InputMethodManager.SHOW_IMPLICIT, MotionEvent.TOOL_TYPE_UNKNOWN,
- null /* resultReceiver */, reason);
+ null /* resultReceiver */, reason);
}
break;
case STATE_SHOW_IME_SNAPSHOT:
- showImeScreenshot(windowToken, displayIdToShowIme);
+ showImeScreenshot(windowToken, displayIdToShowIme, userId);
break;
case STATE_REMOVE_IME_SNAPSHOT:
- removeImeScreenshot(displayIdToShowIme);
+ removeImeScreenshot(displayIdToShowIme, userId);
break;
default:
throw new IllegalArgumentException("Invalid IME visibility state: " + state);
@@ -215,10 +217,11 @@
@GuardedBy("ImfLock.class")
@Override
- public boolean showImeScreenshot(@NonNull IBinder imeTarget, int displayId) {
+ public boolean showImeScreenshot(@NonNull IBinder imeTarget, int displayId,
+ @UserIdInt int userId) {
if (mImeTargetVisibilityPolicy.showImeScreenshot(imeTarget, displayId)) {
mService.onShowHideSoftInputRequested(false /* show */, imeTarget,
- SHOW_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */);
+ SHOW_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */, userId);
return true;
}
return false;
@@ -226,11 +229,11 @@
@GuardedBy("ImfLock.class")
@Override
- public boolean removeImeScreenshot(int displayId) {
+ public boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
if (mImeTargetVisibilityPolicy.removeImeScreenshot(displayId)) {
mService.onShowHideSoftInputRequested(false /* show */,
mService.mImeBindingState.mFocusedWindow,
- REMOVE_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */);
+ REMOVE_IME_SCREENSHOT_FROM_IMMS, null /* statsToken */, userId);
return true;
}
return false;
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 62adb25..a6b07de 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -19,7 +19,6 @@
import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UserIdInt;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -34,24 +33,13 @@
@GuardedBy("ImfLock.class")
private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>();
- @UserIdInt
- private final int mUserId;
-
- @AnyThread
- @UserIdInt
- int getUserId() {
- return mUserId;
- }
-
- HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) {
- mUserId = userId;
- reset(methodMap);
+ HardwareKeyboardShortcutController(@NonNull InputMethodSettings settings) {
+ update(settings);
}
@GuardedBy("ImfLock.class")
- void reset(@NonNull InputMethodMap methodMap) {
+ void update(@NonNull InputMethodSettings settings) {
mSubtypeHandles.clear();
- final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId);
final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList();
for (int i = 0; i < inputMethods.size(); ++i) {
final InputMethodInfo imi = inputMethods.get(i);
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
index a5f9b7a..c1069f2 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
@@ -33,38 +33,45 @@
/**
* Performs showing IME on top of the given window.
*
- * @param showInputToken A token that represents the requester to show IME.
- * @param statsToken The token tracking the current IME request.
- * @param resultReceiver If non-null, this will be called back to the caller when
- * it has processed request to tell what it has done.
- * @param reason The reason for requesting to show IME.
+ * @param showInputToken a token that represents the requester to show IME
+ * @param statsToken the token tracking the current IME request
+ * @param resultReceiver if non-null, this will be called back to the caller when
+ * it has processed request to tell what it has done
+ * @param reason yhe reason for requesting to show IME
+ * @param userId the target user when performing show IME
*/
default void performShowIme(IBinder showInputToken, @NonNull ImeTracker.Token statsToken,
@InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
- @SoftInputShowHideReason int reason) {}
+ @SoftInputShowHideReason int reason, @UserIdInt int userId) {
+ }
/**
* Performs hiding IME to the given window
*
- * @param hideInputToken A token that represents the requester to hide IME.
- * @param statsToken The token tracking the current IME request.
- * @param resultReceiver If non-null, this will be called back to the caller when
- * it has processed request to tell what it has done.
- * @param reason The reason for requesting to hide IME.
+ * @param hideInputToken a token that represents the requester to hide IME
+ * @param statsToken the token tracking the current IME request
+ * @param resultReceiver if non-null, this will be called back to the caller when
+ * it has processed request to tell what it has done
+ * @param reason the reason for requesting to hide IME
+ * @param userId the target user when performing hide IME
*/
default void performHideIme(IBinder hideInputToken, @NonNull ImeTracker.Token statsToken,
- ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
+ ResultReceiver resultReceiver, @SoftInputShowHideReason int reason,
+ @UserIdInt int userId) {
+ }
/**
* Applies the IME visibility from {@link android.inputmethodservice.InputMethodService} with
* according to the given visibility state.
*
- * @param windowToken The token of a window for applying the IME visibility
- * @param statsToken The token tracking the current IME request.
- * @param state The new IME visibility state for the applier to handle
+ * @param windowToken the token of a window for applying the IME visibility
+ * @param statsToken the token tracking the current IME request
+ * @param state the new IME visibility state for the applier to handle
+ * @param userId the target user when applying the IME visibility state
*/
default void applyImeVisibility(IBinder windowToken, @NonNull ImeTracker.Token statsToken,
- @ImeVisibilityStateComputer.VisibilityState int state, @UserIdInt int userId) {}
+ @ImeVisibilityStateComputer.VisibilityState int state, @UserIdInt int userId) {
+ }
/**
* Updates the IME Z-ordering relative to the given window.
@@ -72,7 +79,7 @@
* This used to adjust the IME relative layer of the window during
* {@link InputMethodManagerService} is in switching IME clients.
*
- * @param windowToken The token of a window to update the Z-ordering relative to the IME.
+ * @param windowToken the token of a window to update the Z-ordering relative to the IME
*/
default void updateImeLayeringByTarget(IBinder windowToken) {
// TODO: add a method in WindowManagerInternal to call DC#updateImeInputAndControlTarget
@@ -82,21 +89,24 @@
/**
* Shows the IME screenshot and attach it to the given IME target window.
*
- * @param windowToken The token of a window to show the IME screenshot.
- * @param displayId The unique id to identify the display
- * @return {@code true} if success, {@code false} otherwise.
+ * @param windowToken the token of a window to show the IME screenshot
+ * @param displayId the unique id to identify the display
+ * @param userId the target user when when showing the IME screenshot
+ * @return {@code true} if success, {@code false} otherwise
*/
- default boolean showImeScreenshot(@NonNull IBinder windowToken, int displayId) {
+ default boolean showImeScreenshot(@NonNull IBinder windowToken, int displayId,
+ @UserIdInt int userId) {
return false;
}
/**
* Removes the IME screenshot on the given display.
*
- * @param displayId The target display of showing IME screenshot.
- * @return {@code true} if success, {@code false} otherwise.
+ * @param displayId the target display of showing IME screenshot
+ * @param userId the target user of showing IME screenshot
+ * @return {@code true} if success, {@code false} otherwise
*/
- default boolean removeImeScreenshot(int displayId) {
+ default boolean removeImeScreenshot(int displayId, @UserIdInt int userId) {
return false;
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 39262c5..440343d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -381,10 +381,6 @@
@NonNull
@MultiUserUnawareField
private InputMethodSubtypeSwitchingController mSwitchingController;
- // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- @NonNull
- @MultiUserUnawareField
- private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
@Nullable
private StatusBarManagerInternal mStatusBarManagerInternal;
@@ -1300,11 +1296,8 @@
final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
- mSwitchingController = new InputMethodSubtypeSwitchingController(context,
- settings.getMethodMap(), settings.getUserId());
- mHardwareKeyboardShortcutController =
- new HardwareKeyboardShortcutController(settings.getMethodMap(),
- settings.getUserId());
+ mSwitchingController = new InputMethodSubtypeSwitchingController(context, settings);
+ getUserData(mCurrentUserId).mHardwareKeyboardShortcutController.update(settings);
mMenuController = new InputMethodMenuController(this);
mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
mVisibilityApplier = new DefaultImeVisibilityApplier(this);
@@ -2936,7 +2929,6 @@
* </li>
* <li>{@link InputMethodBindingController#getDeviceIdToShowIme()} is ignored.</li>
* <li>{@link #mSwitchingController} is ignored.</li>
- * <li>{@link #mHardwareKeyboardShortcutController} is ignored.</li>
* <li>{@link #mPreventImeStartupUnlessTextEditor} is ignored.</li>
* <li>and so on.</li>
* </ul>
@@ -2969,6 +2961,9 @@
id = imi.getId();
settings.putSelectedInputMethod(id);
}
+
+ final var userData = getUserData(userId);
+ userData.mHardwareKeyboardShortcutController.update(settings);
}
@GuardedBy("ImfLock.class")
@@ -3046,18 +3041,11 @@
// TODO: Instantiate mSwitchingController for each user.
if (userId == mSwitchingController.getUserId()) {
- mSwitchingController.resetCircularListLocked(settings.getMethodMap());
+ mSwitchingController.resetCircularListLocked(settings);
} else {
- mSwitchingController = new InputMethodSubtypeSwitchingController(mContext,
- settings.getMethodMap(), userId);
+ mSwitchingController = new InputMethodSubtypeSwitchingController(mContext, settings);
}
- // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (userId == mHardwareKeyboardShortcutController.getUserId()) {
- mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
- } else {
- mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- settings.getMethodMap(), userId);
- }
+ getUserData(userId).mHardwareKeyboardShortcutController.update(settings);
sendOnNavButtonFlagsChangedLocked();
}
@@ -3519,10 +3507,11 @@
mVisibilityStateComputer.requestImeVisibility(windowToken, true);
+ final int userId = mCurrentUserId;
// Ensure binding the connection when IME is going to show.
- final var bindingController = getInputMethodBindingController(mCurrentUserId);
+ final var bindingController = getInputMethodBindingController(userId);
bindingController.setCurrentMethodVisible();
- final IInputMethodInvoker curMethod = getCurMethodLocked();
+ final IInputMethodInvoker curMethod = bindingController.getCurMethod();
ImeTracker.forLogging().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME);
final boolean readyToDispatchToIme;
if (Flags.deferShowSoftInputUntilSessionCreation()) {
@@ -3542,7 +3531,7 @@
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
- resultReceiver, reason);
+ resultReceiver, reason, userId);
mVisibilityStateComputer.setInputShown(true);
return true;
} else {
@@ -3654,7 +3643,9 @@
// since Android Eclair. That's why we need to accept IMM#hideSoftInput() even when only
// IMMS#InputShown indicates that the software keyboard is shown.
// TODO(b/246309664): Clean up IMMS#mImeWindowVis
- IInputMethodInvoker curMethod = getCurMethodLocked();
+ final int userId = mCurrentUserId;
+ final var bindingController = getInputMethodBindingController(userId);
+ IInputMethodInvoker curMethod = bindingController.getCurMethod();
final boolean shouldHideSoftInput = curMethod != null
&& (isInputShownLocked() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);
@@ -3665,11 +3656,11 @@
// IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in
// the final state.
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
- mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason);
+ mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason,
+ userId);
} else {
ImeTracker.forLogging().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE);
}
- final var bindingController = getInputMethodBindingController(mCurrentUserId);
bindingController.setCurrentMethodNotVisible();
mVisibilityStateComputer.clearImeShowFlags();
// Cancel existing statsToken for show IME as we got a hide request.
@@ -4735,12 +4726,14 @@
*/
@GuardedBy("ImfLock.class")
void onShowHideSoftInputRequested(boolean show, IBinder requestImeToken,
- @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) {
+ @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken,
+ @UserIdInt int userId) {
final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(requestImeToken);
+ final var bindingController = getInputMethodBindingController(userId);
final WindowManagerInternal.ImeTargetInfo info =
mWindowManagerInternal.onToggleImeRequested(
show, mImeBindingState.mFocusedWindow, requestToken,
- getCurTokenDisplayIdLocked());
+ bindingController.getCurTokenDisplayId());
mSoftInputShowHideHistory.addEntry(new SoftInputShowHideHistory.Entry(
mImeBindingState.mFocusedWindowClient, mImeBindingState.mFocusedWindowEditorInfo,
info.focusedWindowName, mImeBindingState.mFocusedWindowSoftInputMode, reason,
@@ -4927,7 +4920,7 @@
final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
.getSortedInputMethodAndSubtypeList(
showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
- mContext, settings.getMethodMap(), settings.getUserId());
+ mContext, settings);
if (imList.isEmpty()) {
Slog.w(TAG, "Show switching menu failed, imList is empty,"
+ " showAuxSubtypes: " + showAuxSubtypes
@@ -5317,18 +5310,11 @@
// TODO: Instantiate mSwitchingController for each user.
if (userId == mSwitchingController.getUserId()) {
- mSwitchingController.resetCircularListLocked(settings.getMethodMap());
+ mSwitchingController.resetCircularListLocked(settings);
} else {
- mSwitchingController = new InputMethodSubtypeSwitchingController(mContext,
- settings.getMethodMap(), mCurrentUserId);
+ mSwitchingController = new InputMethodSubtypeSwitchingController(mContext, settings);
}
- // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (userId == mHardwareKeyboardShortcutController.getUserId()) {
- mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
- } else {
- mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- settings.getMethodMap(), userId);
- }
+ getUserData(userId).mHardwareKeyboardShortcutController.update(settings);
sendOnNavButtonFlagsChangedLocked();
@@ -5639,8 +5625,8 @@
final InputMethodSubtypeHandle currentSubtypeHandle =
InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype());
final InputMethodSubtypeHandle nextSubtypeHandle =
- mHardwareKeyboardShortcutController.onSubtypeSwitch(currentSubtypeHandle,
- direction > 0);
+ getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
+ currentSubtypeHandle, direction > 0);
if (nextSubtypeHandle == null) {
return;
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index bf9621f..f97a516 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -163,13 +163,12 @@
@NonNull
static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu,
- @NonNull Context context, @NonNull InputMethodMap methodMap,
- @UserIdInt int userId) {
+ @NonNull Context context, @NonNull InputMethodSettings settings) {
+ final int userId = settings.getUserId();
final Context userAwareContext = context.getUserId() == userId
? context
: context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag();
- final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId);
final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList();
if (imis.isEmpty()) {
@@ -487,13 +486,13 @@
private ControllerImpl mController;
InputMethodSubtypeSwitchingController(@NonNull Context context,
- @NonNull InputMethodMap methodMap, @UserIdInt int userId) {
+ @NonNull InputMethodSettings settings) {
mContext = context;
- mUserId = userId;
+ mUserId = settings.getUserId();
mController = ControllerImpl.createFrom(null,
getSortedInputMethodAndSubtypeList(
false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
- false /* forImeMenu */, context, methodMap, userId));
+ false /* forImeMenu */, context, settings));
}
@AnyThread
@@ -507,11 +506,11 @@
mController.onUserActionLocked(imi, subtype);
}
- public void resetCircularListLocked(@NonNull InputMethodMap methodMap) {
+ public void resetCircularListLocked(@NonNull InputMethodSettings settings) {
mController = ControllerImpl.createFrom(mController,
getSortedInputMethodAndSubtypeList(
false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
- false /* forImeMenu */, mContext, methodMap, mUserId));
+ false /* forImeMenu */, mContext, settings));
}
@Nullable
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 2b19d3e..5da4e89 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -88,6 +88,9 @@
@NonNull
final InputMethodBindingController mBindingController;
+ @NonNull
+ final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
+
/**
* Intended to be instantiated only from this file.
*/
@@ -95,6 +98,8 @@
@NonNull InputMethodBindingController bindingController) {
mUserId = userId;
mBindingController = bindingController;
+ mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
+ InputMethodSettings.createEmptyMap(userId));
}
@Override
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 363a4a7..91a4d6f 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -1126,7 +1126,7 @@
}
}
- private void sendHostEndpointConnectedEvent() {
+ void sendHostEndpointConnectedEvent() {
HostEndpointInfo info = new HostEndpointInfo();
info.hostEndpointId = (char) mHostEndPointId;
info.packageName = mPackage;
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index b3fb147..7a722bc 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -74,6 +74,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -158,10 +159,8 @@
// A queue of reliable message records for duplicate detection
private final PriorityQueue<ReliableMessageRecord> mReliableMessageRecordQueue =
- new PriorityQueue<ReliableMessageRecord>(
- (ReliableMessageRecord left, ReliableMessageRecord right) -> {
- return Long.compare(left.getTimestamp(), right.getTimestamp());
- });
+ new PriorityQueue<>(
+ Comparator.comparingLong(ReliableMessageRecord::getTimestamp));
// The test mode manager that manages behaviors during test mode.
private final TestModeManager mTestModeManager = new TestModeManager();
@@ -179,10 +178,10 @@
private boolean mIsBtMainEnabled = false;
// True if test mode is enabled for the Context Hub
- private AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
+ private final AtomicBoolean mIsTestModeEnabled = new AtomicBoolean(false);
// A hashmap used to record if a contexthub is waiting for daily query
- private Set<Integer> mMetricQueryPendingContextHubIds =
+ private final Set<Integer> mMetricQueryPendingContextHubIds =
Collections.newSetFromMap(new ConcurrentHashMap<Integer, Boolean>());
// Lock object for sendWifiSettingUpdate()
@@ -242,10 +241,14 @@
@Override
public void handleServiceRestart() {
- Log.i(TAG, "Starting Context Hub Service restart");
+ Log.i(TAG, "Recovering from Context Hub HAL restart...");
initExistingCallbacks();
resetSettings();
- Log.i(TAG, "Finished Context Hub Service restart");
+ if (Flags.reconnectHostEndpointsAfterHalRestart()) {
+ mClientManager.forEachClientOfHub(mContextHubId,
+ ContextHubClientBroker::sendHostEndpointConnectedEvent);
+ }
+ Log.i(TAG, "Finished recovering from Context Hub HAL restart");
}
@Override
@@ -317,11 +320,11 @@
*/
private static final int MAX_PROBABILITY_PERCENT = 100;
- private Random mRandom = new Random();
+ private final Random mRandom = new Random();
/**
- * @see ContextHubServiceCallback.handleNanoappMessage
* @return whether the message was handled
+ * @see ContextHubServiceCallback#handleNanoappMessage
*/
public boolean handleNanoappMessage(int contextHubId,
short hostEndpointId, NanoAppMessage message,
@@ -331,7 +334,8 @@
}
if (Flags.reliableMessageDuplicateDetectionService()
- && didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) {
+ && mRandom.nextInt(MAX_PROBABILITY_PERCENT)
+ < MESSAGE_DUPLICATION_PROBABILITY_PERCENT) {
Log.i(TAG, "[TEST MODE] Duplicating message ("
+ NUM_MESSAGES_TO_DUPLICATE
+ " sends) with message sequence number: "
@@ -344,16 +348,6 @@
}
return false;
}
-
- /**
- * Returns true if the event with percentPercent did happen.
- *
- * @param probabilityPercent the percent probability of the event.
- * @return true if the event happened, false otherwise.
- */
- private boolean didEventHappen(int probabilityPercent) {
- return mRandom.nextInt(MAX_PROBABILITY_PERCENT) < probabilityPercent;
- }
}
public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
@@ -476,7 +470,7 @@
hubInfo = mContextHubWrapper.getHubs();
} catch (RemoteException e) {
Log.e(TAG, "RemoteException while getting Context Hub info", e);
- hubInfo = new Pair(Collections.emptyList(), Collections.emptyList());
+ hubInfo = new Pair<>(Collections.emptyList(), Collections.emptyList());
}
long bootTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
@@ -536,6 +530,7 @@
for (int contextHubId : mContextHubIdToInfoMap.keySet()) {
try {
mContextHubWrapper.registerExistingCallback(contextHubId);
+ Log.i(TAG, "Re-registered callback to context hub " + contextHubId);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException while registering existing service callback for hub "
+ "(ID = " + contextHubId + ")", e);
@@ -647,7 +642,7 @@
mSensorPrivacyManagerInternal.addSensorPrivacyListenerForAllUsers(
SensorPrivacyManager.Sensors.MICROPHONE, (userId, enabled) -> {
// If we are in HSUM mode, any user can change the microphone setting
- if (mUserManager.isHeadlessSystemUserMode() || userId == getCurrentUserId()) {
+ if (UserManager.isHeadlessSystemUserMode() || userId == getCurrentUserId()) {
Log.d(TAG, "User: " + userId + " mic privacy: " + enabled);
sendMicrophoneDisableSettingUpdate(enabled);
}
@@ -720,33 +715,30 @@
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public int[] getContextHubHandles() throws RemoteException {
+ public int[] getContextHubHandles() {
super.getContextHubHandles_enforcePermission();
-
return ContextHubServiceUtil.createPrimitiveIntArray(mContextHubIdToInfoMap.keySet());
}
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public ContextHubInfo getContextHubInfo(int contextHubHandle) throws RemoteException {
+ public ContextHubInfo getContextHubInfo(int contextHubHandle) {
super.getContextHubInfo_enforcePermission();
-
if (!mContextHubIdToInfoMap.containsKey(contextHubHandle)) {
Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in getContextHubInfo");
return null;
}
-
return mContextHubIdToInfoMap.get(contextHubHandle);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Returns a List of ContextHubInfo object describing the available hubs.
*
* @return the List of ContextHubInfo objects
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public List<ContextHubInfo> getContextHubs() throws RemoteException {
+ public List<ContextHubInfo> getContextHubs() {
super.getContextHubs_enforcePermission();
return mContextHubInfoList;
@@ -814,7 +806,7 @@
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) throws RemoteException {
+ public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) {
super.loadNanoApp_enforcePermission();
if (mContextHubWrapper == null) {
@@ -843,7 +835,7 @@
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public int unloadNanoApp(int nanoAppHandle) throws RemoteException {
+ public int unloadNanoApp(int nanoAppHandle) {
super.unloadNanoApp_enforcePermission();
if (mContextHubWrapper == null) {
@@ -870,7 +862,7 @@
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) throws RemoteException {
+ public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
super.getNanoAppInstanceInfo_enforcePermission();
@@ -880,7 +872,7 @@
@android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public int[] findNanoAppOnHub(
- int contextHubHandle, NanoAppFilter filter) throws RemoteException {
+ int contextHubHandle, NanoAppFilter filter) {
super.findNanoAppOnHub_enforcePermission();
@@ -895,20 +887,19 @@
int[] retArray = new int[foundInstances.size()];
for (int i = 0; i < foundInstances.size(); i++) {
- retArray[i] = foundInstances.get(i).intValue();
+ retArray[i] = foundInstances.get(i);
}
return retArray;
}
/**
* Performs a query at the specified hub.
- * <p>
- * This method should only be invoked internally by the service, either to update the service
+ *
+ * <p>This method should only be invoked internally by the service, either to update the service
* cache or as a result of an explicit query requested by a client through the sendMessage API.
*
* @param contextHubId the ID of the hub to do the query
* @return true if the query succeeded
- * @throws IllegalStateException if the transaction queue is full
*/
private boolean queryNanoAppsInternal(int contextHubId) {
if (mContextHubWrapper == null) {
@@ -1003,7 +994,7 @@
return;
}
- byte errorCode = ErrorCode.OK;
+ byte errorCode;
synchronized (mReliableMessageRecordQueue) {
Optional<ReliableMessageRecord> record =
findReliableMessageRecord(contextHubId,
@@ -1219,7 +1210,6 @@
return mContextHubIdToInfoMap.containsKey(contextHubId);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Creates and registers a client at the service for the specified Context Hub.
*
@@ -1232,10 +1222,11 @@
* @throws IllegalStateException if max number of clients have already registered
* @throws NullPointerException if clientCallback is null
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public IContextHubClient createClient(
int contextHubId, IContextHubClientCallback clientCallback,
- @Nullable String attributionTag, String packageName) throws RemoteException {
+ @Nullable String attributionTag, String packageName) {
super.createClient_enforcePermission();
if (!isValidContextHubId(contextHubId)) {
@@ -1250,7 +1241,6 @@
contextHubInfo, clientCallback, attributionTag, mTransactionManager, packageName);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Creates and registers a PendingIntent client at the service for the specified Context Hub.
*
@@ -1262,10 +1252,11 @@
* @throws IllegalArgumentException if hubInfo does not represent a valid hub
* @throws IllegalStateException if there were too many registered clients at the service
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public IContextHubClient createPendingIntentClient(
int contextHubId, PendingIntent pendingIntent, long nanoAppId,
- @Nullable String attributionTag) throws RemoteException {
+ @Nullable String attributionTag) {
super.createPendingIntentClient_enforcePermission();
if (!isValidContextHubId(contextHubId)) {
@@ -1277,15 +1268,14 @@
contextHubInfo, pendingIntent, nanoAppId, attributionTag, mTransactionManager);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Loads a nanoapp binary at the specified Context hub.
*
* @param contextHubId the ID of the hub to load the binary
* @param transactionCallback the client-facing transaction callback interface
* @param nanoAppBinary the binary to load
- * @throws IllegalStateException if the transaction queue is full
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public void loadNanoAppOnHub(
int contextHubId, IContextHubTransactionCallback transactionCallback,
@@ -1308,15 +1298,14 @@
mTransactionManager.addTransaction(transaction);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Unloads a nanoapp from the specified Context Hub.
*
* @param contextHubId the ID of the hub to unload the nanoapp
* @param transactionCallback the client-facing transaction callback interface
* @param nanoAppId the ID of the nanoapp to unload
- * @throws IllegalStateException if the transaction queue is full
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public void unloadNanoAppFromHub(
int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
@@ -1333,19 +1322,17 @@
mTransactionManager.addTransaction(transaction);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Enables a nanoapp at the specified Context Hub.
*
* @param contextHubId the ID of the hub to enable the nanoapp
* @param transactionCallback the client-facing transaction callback interface
* @param nanoAppId the ID of the nanoapp to enable
- * @throws IllegalStateException if the transaction queue is full
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public void enableNanoApp(
- int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
- throws RemoteException {
+ int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId) {
super.enableNanoApp_enforcePermission();
if (!checkHalProxyAndContextHubId(
@@ -1358,19 +1345,17 @@
mTransactionManager.addTransaction(transaction);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Disables a nanoapp at the specified Context Hub.
*
* @param contextHubId the ID of the hub to disable the nanoapp
* @param transactionCallback the client-facing transaction callback interface
* @param nanoAppId the ID of the nanoapp to disable
- * @throws IllegalStateException if the transaction queue is full
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public void disableNanoApp(
- int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId)
- throws RemoteException {
+ int contextHubId, IContextHubTransactionCallback transactionCallback, long nanoAppId) {
super.disableNanoApp_enforcePermission();
if (!checkHalProxyAndContextHubId(
@@ -1383,17 +1368,16 @@
mTransactionManager.addTransaction(transaction);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Queries for a list of nanoapps from the specified Context hub.
*
* @param contextHubId the ID of the hub to query
* @param transactionCallback the client-facing transaction callback interface
- * @throws IllegalStateException if the transaction queue is full
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public void queryNanoApps(int contextHubId, IContextHubTransactionCallback transactionCallback)
- throws RemoteException {
+ public void queryNanoApps(int contextHubId,
+ IContextHubTransactionCallback transactionCallback) {
super.queryNanoApps_enforcePermission();
if (!checkHalProxyAndContextHubId(
@@ -1406,16 +1390,15 @@
mTransactionManager.addTransaction(transaction);
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Queries for a list of preloaded nanoapp IDs from the specified Context Hub.
*
* @param hubInfo The Context Hub to query a list of nanoapps from.
* @return The list of 64-bit IDs of the preloaded nanoapps.
- * @throws NullPointerException if hubInfo is null
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
- public long[] getPreloadedNanoAppIds(ContextHubInfo hubInfo) throws RemoteException {
+ public long[] getPreloadedNanoAppIds(ContextHubInfo hubInfo) {
super.getPreloadedNanoAppIds_enforcePermission();
Objects.requireNonNull(hubInfo, "hubInfo cannot be null");
@@ -1426,7 +1409,6 @@
return nanoappIds;
}
- @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
/**
* Puts the context hub in and out of test mode. Test mode is a clean state
* where tests can be executed in the same environment. If enable is true,
@@ -1442,6 +1424,7 @@
* test mode.
* @return If true, the operation was successful; false otherwise.
*/
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@Override
public boolean setTestMode(boolean enable) {
super.setTestMode_enforcePermission();
@@ -1551,10 +1534,6 @@
}
}
- private void checkPermissions() {
- ContextHubServiceUtil.checkPermissions(mContext);
- }
-
private int onMessageReceiptOldApi(
int msgType, int contextHubHandle, int appInstance, byte[] data) {
if (data == null) {
@@ -1586,7 +1565,6 @@
callback.onMessageReceipt(contextHubHandle, appInstance, msg);
} catch (RemoteException e) {
Log.i(TAG, "Exception (" + e + ") calling remote callback (" + callback + ").");
- continue;
}
}
mCallbacksList.finishBroadcast();
@@ -1729,8 +1707,8 @@
* Hub.
*/
private void sendMicrophoneDisableSettingUpdateForCurrentUser() {
- boolean isEnabled = mSensorPrivacyManagerInternal == null ? false :
- mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
+ boolean isEnabled = mSensorPrivacyManagerInternal != null
+ && mSensorPrivacyManagerInternal.isSensorPrivacyEnabled(
getCurrentUserId(), SensorPrivacyManager.Sensors.MICROPHONE);
sendMicrophoneDisableSettingUpdate(isEnabled);
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f297708..a4f534e 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -8565,6 +8565,13 @@
*/
private boolean enqueueNotification() {
synchronized (mNotificationLock) {
+ if (android.app.Flags.secureAllowlistToken()) {
+ // allowlistToken is populated by unparceling, so it will be absent if the
+ // EnqueueNotificationRunnable is created directly by NMS (as we do for group
+ // summaries) instead of via notify(). Fix that.
+ r.getNotification().overrideAllowlistToken(ALLOWLIST_TOKEN);
+ }
+
final long snoozeAt =
mSnoozeHelper.getSnoozeTimeForUnpostedNotification(
r.getUser().getIdentifier(),
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index d1d8993..7f4a5cb 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -62,6 +62,8 @@
import android.app.IActivityManager;
import android.app.IStopUserCallback;
import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.StatsManager;
import android.app.admin.DevicePolicyEventLogger;
@@ -147,6 +149,8 @@
import com.android.internal.app.IAppOpsService;
import com.android.internal.app.SetScreenLockDialogActivity;
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.RoSystemProperties;
import com.android.internal.util.DumpUtils;
@@ -1070,6 +1074,8 @@
if (isAutoLockingPrivateSpaceOnRestartsEnabled()) {
autoLockPrivateSpace();
}
+
+ showHsumNotificationIfNeeded();
}
private boolean isAutoLockingPrivateSpaceOnRestartsEnabled() {
@@ -4163,6 +4169,48 @@
mUpdatingSystemUserMode = true;
}
+ /**
+ * If the device's actual HSUM status differs from that which is defined by its build
+ * configuration, warn the user. Ignores HSUM emulated status, since that isn't relevant.
+ *
+ * The goal is to inform dogfooders that they need to factory reset the device to align their
+ * device with its build configuration.
+ */
+ private void showHsumNotificationIfNeeded() {
+ if (RoSystemProperties.MULTIUSER_HEADLESS_SYSTEM_USER == isHeadlessSystemUserMode()) {
+ // Actual state does match the configuration. Great!
+ return;
+ }
+ if (Build.isDebuggable()
+ && !TextUtils.isEmpty(SystemProperties.get(SYSTEM_USER_MODE_EMULATION_PROPERTY))) {
+ // Ignore any device that has been playing around with HSUM emulation.
+ return;
+ }
+ Slogf.w(LOG_TAG, "Posting warning that device's HSUM status doesn't match the build's.");
+
+ final String title = mContext
+ .getString(R.string.wrong_hsum_configuration_notification_title);
+ final String message = mContext
+ .getString(R.string.wrong_hsum_configuration_notification_message);
+
+ final Notification notification =
+ new Notification.Builder(mContext, SystemNotificationChannels.DEVELOPER)
+ .setSmallIcon(R.drawable.stat_sys_adb)
+ .setWhen(0)
+ .setOngoing(true)
+ .setTicker(title)
+ .setDefaults(0)
+ .setColor(mContext.getColor(R.color.system_notification_accent_color))
+ .setContentTitle(title)
+ .setContentText(message)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .build();
+
+ final NotificationManager notificationManager =
+ mContext.getSystemService(NotificationManager.class);
+ notificationManager.notifyAsUser(
+ null, SystemMessage.NOTE_WRONG_HSUM_STATUS, notification, UserHandle.ALL);
+ }
private ResilientAtomicFile getUserListFile() {
File tempBackup = new File(mUserListFile.getParent(), mUserListFile.getName() + ".backup");
diff --git a/services/core/java/com/android/server/power/OWNERS b/services/core/java/com/android/server/power/OWNERS
index 94340ec..c1fad33 100644
--- a/services/core/java/com/android/server/power/OWNERS
+++ b/services/core/java/com/android/server/power/OWNERS
@@ -1,6 +1,7 @@
michaelwr@google.com
santoscordon@google.com
-philipjunker@google.com
+petsjonkin@google.com
+brup@google.com
per-file ThermalManagerService.java=file:/THERMAL_OWNERS
per-file LowPowerStandbyController.java=qingxun@google.com
diff --git a/services/core/java/com/android/server/timezonedetector/OWNERS b/services/core/java/com/android/server/timezonedetector/OWNERS
index dfa07d8..4220d14 100644
--- a/services/core/java/com/android/server/timezonedetector/OWNERS
+++ b/services/core/java/com/android/server/timezonedetector/OWNERS
@@ -1,7 +1,6 @@
# Bug component: 847766
# This is the main list for platform time / time zone detection maintainers, for this dir and
# ultimately referenced by other OWNERS files for components maintained by the same team.
-nfuller@google.com
boullanger@google.com
jmorace@google.com
kanyinsola@google.com
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
index 9756094..503a726 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
@@ -108,9 +108,9 @@
throws CustomizationParserException, IOException {
try {
return loadVibrationsInternal(res, vibratorInfo);
- } catch (VibrationXmlParser.VibrationXmlParserException
- | XmlParserException
- | XmlPullParserException e) {
+ } catch (VibrationXmlParser.ParseFailedException
+ | XmlParserException
+ | XmlPullParserException e) {
throw new CustomizationParserException(
"Error parsing haptic feedback customization file.", e);
}
@@ -121,7 +121,6 @@
Resources res, VibratorInfo vibratorInfo) throws
CustomizationParserException,
IOException,
- VibrationXmlParser.VibrationXmlParserException,
XmlParserException,
XmlPullParserException {
if (!Flags.hapticFeedbackVibrationOemCustomizationEnabled()) {
@@ -172,10 +171,6 @@
ParsedVibration parsedVibration = VibrationXmlParser.parseElement(
parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
- if (parsedVibration == null) {
- throw new CustomizationParserException(
- "Unable to parse vibration element for effect " + effectId);
- }
VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
if (effect != null) {
if (effect.getDuration() == Long.MAX_VALUE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 7f60dc44..5c15ccb 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -2494,9 +2494,6 @@
try {
ParsedVibration parsedVibration =
VibrationXmlParser.parseDocument(new StringReader(xml));
- if (parsedVibration == null) {
- throw new IllegalArgumentException("Error parsing vibration XML " + xml);
- }
VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo();
if (combinedVibratorInfo == null) {
throw new IllegalStateException(
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index b89120b..b846947 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -650,9 +650,12 @@
synchronized (mLock) {
if (mLastWallpaper != null) {
WallpaperData targetWallpaper = null;
- if (mLastWallpaper.connection.containsDisplay(displayId)) {
+ if (mLastWallpaper.connection != null &&
+ mLastWallpaper.connection.containsDisplay(displayId)) {
targetWallpaper = mLastWallpaper;
- } else if (mFallbackWallpaper.connection.containsDisplay(displayId)) {
+ } else if (mFallbackWallpaper != null &&
+ mFallbackWallpaper.connection != null &&
+ mFallbackWallpaper.connection.containsDisplay(displayId)) {
targetWallpaper = mFallbackWallpaper;
}
if (targetWallpaper == null) return;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9bc4389..21155bb 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6975,14 +6975,11 @@
updateReportedVisibilityLocked();
}
- /**
- * Sets whether something has been visible in the task and returns {@code true} if the state
- * is changed from invisible to visible.
- */
- private boolean setTaskHasBeenVisible() {
+ /** Sets whether something has been visible in the task. */
+ private void setTaskHasBeenVisible() {
final boolean wasTaskVisible = task.getHasBeenVisible();
if (wasTaskVisible) {
- return false;
+ return;
}
if (inTransition()) {
// The deferring will be canceled until transition is ready so it won't dispatch
@@ -6990,20 +6987,22 @@
task.setDeferTaskAppear(true);
}
task.setHasBeenVisible(true);
- return true;
}
void onStartingWindowDrawn() {
- boolean wasTaskVisible = false;
if (task != null) {
mSplashScreenStyleSolidColor = true;
- wasTaskVisible = !setTaskHasBeenVisible();
+ setTaskHasBeenVisible();
}
+ if (mStartingData == null || mStartingData.mIsDisplayed) {
+ return;
+ }
+ mStartingData.mIsDisplayed = true;
// The transition may not be executed if the starting process hasn't attached. But if the
// starting window is drawn, the transition can start earlier. Exclude finishing and bubble
// because it may be a trampoline.
- if (!wasTaskVisible && mStartingData != null && !finishing && !mLaunchedFromBubble
+ if (app == null && !finishing && !mLaunchedFromBubble
&& mVisibleRequested && !mDisplayContent.mAppTransition.isReady()
&& !mDisplayContent.mAppTransition.isRunning()
&& mDisplayContent.isNextTransitionForward()) {
@@ -7240,9 +7239,6 @@
isInterestingAndDrawn = true;
}
}
- } else if (mStartingData != null && w.isDrawn()) {
- // The starting window for this container is drawn.
- mStartingData.mIsDisplayed = true;
}
}
@@ -7526,7 +7522,8 @@
* use an icon or solid color splash screen will be made by WmShell.
*/
private boolean shouldUseSolidColorSplashScreen(ActivityRecord sourceRecord,
- boolean startActivity, ActivityOptions options, int resolvedTheme) {
+ boolean startActivity, ActivityOptions options, int resolvedTheme,
+ boolean newTask) {
if (sourceRecord == null && !startActivity) {
// Use simple style if this activity is not top activity. This could happen when adding
// a splash screen window to the warm start activity which is re-create because top is
@@ -7549,21 +7546,19 @@
// Choose the default behavior when neither the ActivityRecord nor the activity theme have
// specified a splash screen style.
-
- if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_HOME || launchedFromUid == Process.SHELL_UID) {
- return false;
- } else if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEMUI) {
+ if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_SYSTEMUI) {
return true;
} else {
// Need to check sourceRecord in case this activity is launched from a service.
if (sourceRecord == null) {
sourceRecord = searchCandidateLaunchingActivity();
}
-
if (sourceRecord != null) {
- return sourceRecord.mSplashScreenStyleSolidColor;
+ return sourceRecord.mSplashScreenStyleSolidColor; // follow previous activity
+ } else if (mLaunchSourceType == LAUNCH_SOURCE_TYPE_HOME
+ || launchedFromUid == Process.SHELL_UID) {
+ return !newTask; // only show icon for new task
}
-
// Use an icon if the activity was launched from System for the first start.
// Otherwise, must use solid color splash screen.
return mLaunchSourceType != LAUNCH_SOURCE_TYPE_SYSTEM || !startActivity;
@@ -7631,7 +7626,7 @@
splashScreenTheme);
mSplashScreenStyleSolidColor = shouldUseSolidColorSplashScreen(sourceRecord, startActivity,
- startOptions, resolvedTheme);
+ startOptions, resolvedTheme, newTask);
final boolean activityCreated =
mState.ordinal() >= STARTED.ordinal() && mState.ordinal() <= STOPPED.ordinal();
@@ -8293,7 +8288,8 @@
*/
@Override
protected int getOverrideOrientation() {
- return mLetterboxUiController.overrideOrientationIfNeeded(super.getOverrideOrientation());
+ return mAppCompatController.getOrientationPolicy()
+ .overrideOrientationIfNeeded(super.getOverrideOrientation());
}
/**
@@ -10825,7 +10821,8 @@
proto.write(SHOULD_OVERRIDE_MIN_ASPECT_RATIO,
mLetterboxUiController.shouldOverrideMinAspectRatio());
proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP,
- mLetterboxUiController.shouldIgnoreOrientationRequestLoop());
+ mAppCompatController.getAppCompatCapability().getAppCompatOrientationCapability()
+ .shouldIgnoreOrientationRequestLoop());
proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP,
mLetterboxUiController.shouldOverrideForceResizeApp());
proto.write(SHOULD_ENABLE_USER_ASPECT_RATIO_SETTINGS,
@@ -11157,8 +11154,7 @@
boolean cancel) {
// This override is just for getting metrics. allFinished needs to be checked before
// finish because finish resets all the states.
- final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
- if (syncGroup != null && group != getSyncGroup()) return;
+ if (isDifferentSyncGroup(group)) return;
mLastAllReadyAtSync = allSyncFinished();
super.finishSync(outMergedTransaction, group, cancel);
}
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index e8faff6..a8cc2ae 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -348,6 +348,11 @@
wc.setSyncGroup(this);
}
wc.prepareSync();
+ if (wc.mSyncState == WindowContainer.SYNC_STATE_NONE && wc.mSyncGroup != null) {
+ Slog.w(TAG, "addToSync: unset SyncGroup " + wc.mSyncGroup.mSyncId
+ + " for non-sync " + wc);
+ wc.mSyncGroup = null;
+ }
if (mReady) {
mWm.mWindowPlacerLocked.requestTraversal();
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 17547f5..e0d20351 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -66,7 +66,6 @@
import android.annotation.Nullable;
import android.app.ActivityManager.TaskDescription;
import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
-import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -151,27 +150,6 @@
}
}
- /**
- * Whether an app is calling {@link android.app.Activity#setRequestedOrientation}
- * in a loop and orientation request should be ignored.
- *
- * <p>This should only be called once in response to
- * {@link android.app.Activity#setRequestedOrientation}. See
- * {@link #shouldIgnoreRequestedOrientation} for more details.
- *
- * <p>This treatment is enabled when the following conditions are met:
- * <ul>
- * <li>Flag gating the treatment is enabled
- * <li>Opt-out component property isn't enabled
- * <li>Per-app override is enabled
- * <li>App has requested orientation more than 2 times within 1-second
- * timer and activity is not letterboxed for fixed orientation
- * </ul>
- */
- boolean shouldIgnoreOrientationRequestLoop() {
- return getAppCompatCapability().getAppCompatOrientationCapability()
- .shouldIgnoreOrientationRequestLoop();
- }
@VisibleForTesting
int getSetOrientationRequestCounter() {
@@ -299,12 +277,6 @@
return getAppCompatCapability().shouldUseDisplayLandscapeNaturalOrientation();
}
- @ScreenOrientation
- int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
- return mActivityRecord.mAppCompatController.getOrientationPolicy()
- .overrideOrientationIfNeeded(candidate);
- }
-
boolean isOverrideOrientationOnlyForCameraEnabled() {
return getAppCompatCapability().isOverrideOrientationOnlyForCameraEnabled();
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index c72087b..9b8c038 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -47,8 +47,6 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.SurfaceControl.METADATA_TASK_ID;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -3586,15 +3584,29 @@
? null : new PictureInPictureParams(top.pictureInPictureArgs);
}
- Rect getDisplayCutoutInsets() {
- if (mDisplayContent == null || getDisplayInfo().displayCutout == null) return null;
+ /** @return The display cutout insets where the main window is not allowed to extend to. */
+ @NonNull Rect getDisplayCutoutInsets() {
+ final Rect displayCutoutInsets = new Rect();
+ if (mDisplayContent == null || getDisplayInfo().displayCutout == null) {
+ return displayCutoutInsets;
+ }
final WindowState w = getTopVisibleAppMainWindow();
- final int displayCutoutMode = w == null
- ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
- : w.getAttrs().layoutInDisplayCutoutMode;
- return (displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
- || displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
- ? null : getDisplayInfo().displayCutout.getSafeInsets();
+ final Rect displayFrame;
+ if (w != null && w.mHaveFrame) {
+ displayFrame = w.getDisplayFrame();
+ } else {
+ displayFrame = mDisplayContent.getBounds();
+ displayFrame.inset(getDisplayInfo().displayCutout.getSafeInsets());
+ }
+ final Rect taskBounds = getBounds();
+ if (displayCutoutInsets.setIntersect(taskBounds, displayFrame)) {
+ displayCutoutInsets.set(
+ displayCutoutInsets.left - taskBounds.left,
+ displayCutoutInsets.top - taskBounds.top,
+ taskBounds.right - displayCutoutInsets.right,
+ taskBounds.bottom - displayCutoutInsets.bottom);
+ }
+ return displayCutoutInsets;
}
/**
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index a029f38..2572128 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2545,7 +2545,8 @@
final ChangeInfo changeInfo = changes.get(wc);
// Reject no-ops, unless wallpaper
- if (!changeInfo.hasChanged() && wc.asWallpaperToken() == null) {
+ if (!changeInfo.hasChanged()
+ && (!Flags.ensureWallpaperInTransitions() || wc.asWallpaperToken() == null)) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
" Rejecting as no-op: %s", wc);
continue;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index a3e0302..86440ac 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -60,6 +60,7 @@
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
+import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -764,10 +765,19 @@
void collectTopWallpapers(Transition transition) {
if (mFindResults.hasTopShowWhenLockedWallpaper()) {
- transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper.mToken);
+ if (Flags.ensureWallpaperInTransitions()) {
+ transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper.mToken);
+ } else {
+ transition.collect(mFindResults.mTopWallpaper.mTopShowWhenLockedWallpaper);
+ }
+
}
if (mFindResults.hasTopHideWhenLockedWallpaper()) {
- transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper.mToken);
+ if (Flags.ensureWallpaperInTransitions()) {
+ transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper.mToken);
+ } else {
+ transition.collect(mFindResults.mTopWallpaper.mTopHideWhenLockedWallpaper);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 6dbd259..1f31af6 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3986,6 +3986,19 @@
}
/**
+ * Returns {@code true} if this window container belongs to a different sync group than the
+ * given group.
+ */
+ boolean isDifferentSyncGroup(@Nullable BLASTSyncEngine.SyncGroup group) {
+ if (group == null) return false;
+ final BLASTSyncEngine.SyncGroup thisGroup = getSyncGroup();
+ if (thisGroup == null || group == thisGroup) return false;
+ Slog.d(TAG, this + " uses a different SyncGroup, current=" + thisGroup.mSyncId
+ + " given=" + group.mSyncId);
+ return true;
+ }
+
+ /**
* Recursively finishes/cleans-up sync state of this subtree and collects all the sync
* transactions into `outMergedTransaction`.
* @param outMergedTransaction A transaction to merge all the recorded sync operations into.
@@ -3994,10 +4007,14 @@
*/
void finishSync(Transaction outMergedTransaction, @Nullable BLASTSyncEngine.SyncGroup group,
boolean cancel) {
- if (mSyncState == SYNC_STATE_NONE) return;
- final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
- // If it's null, then we need to clean-up anyways.
- if (syncGroup != null && group != syncGroup) return;
+ if (mSyncState == SYNC_STATE_NONE) {
+ if (mSyncGroup != null) {
+ Slog.e(TAG, "finishSync: stale group " + mSyncGroup.mSyncId + " of " + this);
+ mSyncGroup = null;
+ }
+ return;
+ }
+ if (isDifferentSyncGroup(group)) return;
ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "finishSync cancel=%b for %s", cancel, this);
outMergedTransaction.merge(mSyncTransaction);
for (int i = mChildren.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index dcd4bd6..9d4a3b8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -96,6 +96,7 @@
import static android.view.WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
+import static android.util.SequenceUtils.getNextSeq;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ANIM;
@@ -3652,6 +3653,7 @@
}
}
outFrames.compatScale = getCompatScaleForClient();
+ outFrames.seq = getNextSeq(mLastReportedFrames.seq);
if (mLastReportedFrames != outFrames) {
mLastReportedFrames.setTo(outFrames);
}
@@ -3682,7 +3684,9 @@
}
void fillInsetsState(@NonNull InsetsState outInsetsState, boolean copySources) {
+ final int lastSeq = mLastReportedInsetsState.getSeq();
outInsetsState.set(getCompatInsetsState(), copySources);
+ outInsetsState.setSeq(getNextSeq(lastSeq));
if (outInsetsState != mLastReportedInsetsState) {
// No need to copy for the recorded.
mLastReportedInsetsState.set(outInsetsState, false /* copySources */);
@@ -3691,9 +3695,11 @@
void fillInsetsSourceControls(@NonNull InsetsSourceControl.Array outArray,
boolean copyControls) {
+ final int lastSeq = mLastReportedInsetsState.getSeq();
final InsetsSourceControl[] controls =
getDisplayContent().getInsetsStateController().getControlsForDispatch(this);
outArray.set(controls, copyControls);
+ outArray.setSeq(getNextSeq(lastSeq));
if (outArray != mLastReportedActiveControls) {
// No need to copy for the recorded.
mLastReportedActiveControls.setTo(outArray, false /* copyControls */);
@@ -5791,8 +5797,7 @@
@Override
void finishSync(Transaction outMergedTransaction, BLASTSyncEngine.SyncGroup group,
boolean cancel) {
- final BLASTSyncEngine.SyncGroup syncGroup = getSyncGroup();
- if (syncGroup != null && group != syncGroup) return;
+ if (isDifferentSyncGroup(group)) return;
mPrepareSyncSeqId = 0;
if (cancel) {
// This is leaving sync so any buffers left in the sync have a chance of
diff --git a/services/fakes/Android.bp b/services/fakes/Android.bp
index 148054b..d44bb5a 100644
--- a/services/fakes/Android.bp
+++ b/services/fakes/Android.bp
@@ -16,5 +16,5 @@
"java/**/*.java",
],
path: "java",
- visibility: ["//frameworks/base"],
+ visibility: ["//frameworks/base/ravenwood:__subpackages__"],
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index a4ca317..d4adba2 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -87,7 +87,7 @@
synchronized (ImfLock.class) {
mVisibilityApplier.performShowIme(new Binder() /* showInputToken */,
ImeTracker.Token.empty(), 0 /* showFlags */, null /* resultReceiver */,
- SHOW_SOFT_INPUT);
+ SHOW_SOFT_INPUT, mUserId);
}
verifyShowSoftInput(false, true, 0 /* showFlags */);
}
@@ -96,7 +96,7 @@
public void testPerformHideIme() throws Exception {
synchronized (ImfLock.class) {
mVisibilityApplier.performHideIme(new Binder() /* hideInputToken */,
- ImeTracker.Token.empty(), null /* resultReceiver */, HIDE_SOFT_INPUT);
+ ImeTracker.Token.empty(), null /* resultReceiver */, HIDE_SOFT_INPUT, mUserId);
}
verifyHideSoftInput(false, true);
}
@@ -186,7 +186,7 @@
@Test
public void testShowImeScreenshot() {
synchronized (ImfLock.class) {
- mVisibilityApplier.showImeScreenshot(mWindowToken, Display.DEFAULT_DISPLAY);
+ mVisibilityApplier.showImeScreenshot(mWindowToken, Display.DEFAULT_DISPLAY, mUserId);
}
verify(mMockImeTargetVisibilityPolicy).showImeScreenshot(eq(mWindowToken),
@@ -196,7 +196,7 @@
@Test
public void testRemoveImeScreenshot() {
synchronized (ImfLock.class) {
- mVisibilityApplier.removeImeScreenshot(Display.DEFAULT_DISPLAY);
+ mVisibilityApplier.removeImeScreenshot(Display.DEFAULT_DISPLAY, mUserId);
}
verify(mMockImeTargetVisibilityPolicy).removeImeScreenshot(eq(Display.DEFAULT_DISPLAY));
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 02b7291..e81cf9d 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -38,7 +38,6 @@
import org.junit.Test;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
public final class InputMethodSubtypeSwitchingControllerTest {
@@ -65,17 +64,17 @@
private static void addTestImeSubtypeListItems(@NonNull List<ImeSubtypeListItem> items,
@NonNull String imeName, @NonNull String imeLabel,
@Nullable List<String> subtypeLocales, boolean supportsSwitchingToNextInputMethod) {
- final ResolveInfo ri = new ResolveInfo();
- final ServiceInfo si = new ServiceInfo();
final ApplicationInfo ai = new ApplicationInfo();
ai.packageName = TEST_PACKAGE_NAME;
ai.enabled = true;
+ final ServiceInfo si = new ServiceInfo();
si.applicationInfo = ai;
si.enabled = true;
si.packageName = TEST_PACKAGE_NAME;
si.name = imeName;
si.exported = true;
si.nonLocalizedLabel = imeLabel;
+ final ResolveInfo ri = new ResolveInfo();
ri.serviceInfo = si;
List<InputMethodSubtype> subtypes = null;
if (subtypeLocales != null) {
@@ -102,8 +101,7 @@
@NonNull
private static ImeSubtypeListItem createTestItem(@NonNull ComponentName imeComponentName,
@NonNull String imeName, @NonNull String subtypeName,
- @NonNull String subtypeLocale, int subtypeIndex,
- @NonNull String systemLocale) {
+ @NonNull String subtypeLocale, int subtypeIndex) {
final var ai = new ApplicationInfo();
ai.packageName = imeComponentName.getPackageName();
ai.enabled = true;
@@ -125,26 +123,26 @@
.build());
final InputMethodInfo imi = new InputMethodInfo(ri, TEST_IS_AUX_IME,
TEST_SETTING_ACTIVITY_NAME, subtypes, TEST_IS_DEFAULT_RES_ID,
- TEST_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */,
- TEST_IS_VR_IME);
+ TEST_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */, TEST_IS_VR_IME);
return new ImeSubtypeListItem(imeName, subtypeName, imi, subtypeIndex, subtypeLocale,
- systemLocale);
+ SYSTEM_LOCALE);
}
@NonNull
private static List<ImeSubtypeListItem> createEnabledImeSubtypes() {
final var items = new ArrayList<ImeSubtypeListItem>();
- addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", Arrays.asList("en_US", "fr"),
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", List.of("en_US", "fr"),
true /* supportsSwitchingToNextInputMethod*/);
addTestImeSubtypeListItems(items, "switchUnawareLatinIme", "switchUnawareLatinIme",
- Arrays.asList("en_UK", "hi"),
- false /* supportsSwitchingToNextInputMethod*/);
+ List.of("en_UK", "hi"), false /* supportsSwitchingToNextInputMethod*/);
+ addTestImeSubtypeListItems(items, "subtypeAwareIme", "subtypeAwareIme", null,
+ true /* supportsSwitchingToNextInputMethod */);
addTestImeSubtypeListItems(items, "subtypeUnawareIme", "subtypeUnawareIme", null,
false /* supportsSwitchingToNextInputMethod*/);
- addTestImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", Arrays.asList("ja_JP"),
+ addTestImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", List.of("ja_JP"),
true /* supportsSwitchingToNextInputMethod*/);
addTestImeSubtypeListItems(items, "switchUnawareJapaneseIme", "switchUnawareJapaneseIme",
- Arrays.asList("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
+ List.of("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
return items;
}
@@ -153,11 +151,11 @@
final var items = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(items,
"UnknownIme", "UnknownIme",
- Arrays.asList("en_US", "hi"),
+ List.of("en_US", "hi"),
true /* supportsSwitchingToNextInputMethod*/);
addTestImeSubtypeListItems(items,
"UnknownSwitchingUnawareIme", "UnknownSwitchingUnawareIme",
- Arrays.asList("en_US"),
+ List.of("en_US"),
false /* supportsSwitchingToNextInputMethod*/);
addTestImeSubtypeListItems(items, "UnknownSubtypeUnawareIme",
"UnknownSubtypeUnawareIme", null,
@@ -209,16 +207,17 @@
final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
final ImeSubtypeListItem switchingUnawareLatinIme_en_uk = enabledItems.get(2);
final ImeSubtypeListItem switchingUnawareLatinIme_hi = enabledItems.get(3);
- final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
- final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(5);
- final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(6);
+ final ImeSubtypeListItem subtypeAwareIme = enabledItems.get(4);
+ final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(5);
+ final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(6);
+ final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
final ControllerImpl controller = ControllerImpl.createFrom(
null /* currentInstance */, enabledItems);
// switching-aware loop
assertRotationOrder(controller, false /* onlyCurrentIme */,
- latinIme_en_us, latinIme_fr, japaneseIme_ja_jp);
+ latinIme_en_us, latinIme_fr, subtypeAwareIme, japaneseIme_ja_jp);
// switching-unaware loop
assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -231,6 +230,8 @@
assertRotationOrder(controller, true /* onlyCurrentIme */,
switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi);
assertNextInputMethod(controller, true /* onlyCurrentIme */,
+ subtypeAwareIme, null);
+ assertNextInputMethod(controller, true /* onlyCurrentIme */,
subtypeUnawareIme, null);
assertNextInputMethod(controller, true /* onlyCurrentIme */,
japaneseIme_ja_jp, null);
@@ -261,55 +262,56 @@
final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
final ImeSubtypeListItem latinIme_en_us = enabledItems.get(0);
final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
- final ImeSubtypeListItem switchingUnawarelatinIme_en_uk = enabledItems.get(2);
- final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3);
- final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
- final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(5);
- final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(6);
+ final ImeSubtypeListItem switchingUnawareLatinIme_en_uk = enabledItems.get(2);
+ final ImeSubtypeListItem switchingUnawareLatinIme_hi = enabledItems.get(3);
+ final ImeSubtypeListItem subtypeAwareIme = enabledItems.get(4);
+ final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(5);
+ final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(6);
+ final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
final ControllerImpl controller = ControllerImpl.createFrom(
null /* currentInstance */, enabledItems);
// === switching-aware loop ===
assertRotationOrder(controller, false /* onlyCurrentIme */,
- latinIme_en_us, latinIme_fr, japaneseIme_ja_jp);
+ latinIme_en_us, latinIme_fr, subtypeAwareIme, japaneseIme_ja_jp);
// Then notify that a user did something for latinIme_fr.
onUserAction(controller, latinIme_fr);
assertRotationOrder(controller, false /* onlyCurrentIme */,
- latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
+ latinIme_fr, latinIme_en_us, subtypeAwareIme, japaneseIme_ja_jp);
// Then notify that a user did something for latinIme_fr again.
onUserAction(controller, latinIme_fr);
assertRotationOrder(controller, false /* onlyCurrentIme */,
- latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
- // Then notify that a user did something for japaneseIme_ja_JP.
- onUserAction(controller, latinIme_fr);
+ latinIme_fr, latinIme_en_us, subtypeAwareIme, japaneseIme_ja_jp);
+ // Then notify that a user did something for subtypeAwareIme.
+ onUserAction(controller, subtypeAwareIme);
assertRotationOrder(controller, false /* onlyCurrentIme */,
- japaneseIme_ja_jp, latinIme_fr, latinIme_en_us);
+ subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
// Check onlyCurrentIme == true.
- assertNextInputMethod(controller, true /* onlyCurrentIme */,
- japaneseIme_ja_jp, null);
assertRotationOrder(controller, true /* onlyCurrentIme */,
latinIme_fr, latinIme_en_us);
- assertRotationOrder(controller, true /* onlyCurrentIme */,
- latinIme_en_us, latinIme_fr);
+ assertNextInputMethod(controller, true /* onlyCurrentIme */,
+ subtypeAwareIme, null);
+ assertNextInputMethod(controller, true /* onlyCurrentIme */,
+ japaneseIme_ja_jp, null);
// === switching-unaware loop ===
assertRotationOrder(controller, false /* onlyCurrentIme */,
- switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+ switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
switchUnawareJapaneseIme_ja_jp);
// User action should be ignored for switching unaware IMEs.
- onUserAction(controller, switchingUnawarelatinIme_hi);
+ onUserAction(controller, switchingUnawareLatinIme_hi);
assertRotationOrder(controller, false /* onlyCurrentIme */,
- switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+ switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
switchUnawareJapaneseIme_ja_jp);
// User action should be ignored for switching unaware IMEs.
- onUserAction(controller, switchUnawareJapaneseIme_ja_jp);
+ onUserAction(controller, subtypeUnawareIme);
assertRotationOrder(controller, false /* onlyCurrentIme */,
- switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+ switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
switchUnawareJapaneseIme_ja_jp);
// Check onlyCurrentIme == true.
assertRotationOrder(controller, true /* onlyCurrentIme */,
- switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi);
+ switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi);
assertNextInputMethod(controller, true /* onlyCurrentIme */,
subtypeUnawareIme, null);
assertNextInputMethod(controller, true /* onlyCurrentIme */,
@@ -320,28 +322,28 @@
final ControllerImpl newController = ControllerImpl.createFrom(controller,
sameEnabledItems);
assertRotationOrder(newController, false /* onlyCurrentIme */,
- japaneseIme_ja_jp, latinIme_fr, latinIme_en_us);
+ subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
assertRotationOrder(newController, false /* onlyCurrentIme */,
- switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+ switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
switchUnawareJapaneseIme_ja_jp);
// Rotation order should be initialized when created with a different subtype list.
- final List<ImeSubtypeListItem> differentEnabledItems = Arrays.asList(
- latinIme_en_us, latinIme_fr, switchingUnawarelatinIme_en_uk,
- switchUnawareJapaneseIme_ja_jp);
+ final List<ImeSubtypeListItem> differentEnabledItems = List.of(
+ latinIme_en_us, latinIme_fr, subtypeAwareIme, switchingUnawareLatinIme_en_uk,
+ switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
final ControllerImpl anotherController = ControllerImpl.createFrom(controller,
differentEnabledItems);
assertRotationOrder(anotherController, false /* onlyCurrentIme */,
- latinIme_en_us, latinIme_fr);
+ latinIme_en_us, latinIme_fr, subtypeAwareIme);
assertRotationOrder(anotherController, false /* onlyCurrentIme */,
- switchingUnawarelatinIme_en_uk, switchUnawareJapaneseIme_ja_jp);
+ switchingUnawareLatinIme_en_uk, switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
}
@Test
public void testImeSubtypeListItem() {
final var items = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
- Arrays.asList("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
+ List.of("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
true /* supportsSwitchingToNextInputMethod*/);
final ImeSubtypeListItem item_en_us = items.get(0);
final ImeSubtypeListItem item_fr = items.get(1);
@@ -376,61 +378,61 @@
final ComponentName imeY1 = new ComponentName("com.example.imeY", "Ime1");
final ComponentName imeZ1 = new ComponentName("com.example.imeZ", "Ime1");
{
- final List<ImeSubtypeListItem> items = Arrays.asList(
+ final List<ImeSubtypeListItem> items = List.of(
// Subtypes of two IMEs that have the same display name "X".
// Subtypes that has the same locale of the system's.
- createTestItem(imeX1, "X", "E", "en_US", 0, "en_US"),
- createTestItem(imeX2, "X", "E", "en_US", 0, "en_US"),
- createTestItem(imeX1, "X", "Z", "en_US", 3, "en_US"),
- createTestItem(imeX2, "X", "Z", "en_US", 3, "en_US"),
- createTestItem(imeX1, "X", "", "en_US", 6, "en_US"),
- createTestItem(imeX2, "X", "", "en_US", 6, "en_US"),
+ createTestItem(imeX1, "X", "E", "en_US", 0),
+ createTestItem(imeX2, "X", "E", "en_US", 0),
+ createTestItem(imeX1, "X", "Z", "en_US", 3),
+ createTestItem(imeX2, "X", "Z", "en_US", 3),
+ createTestItem(imeX1, "X", "", "en_US", 6),
+ createTestItem(imeX2, "X", "", "en_US", 6),
// Subtypes that has the same language of the system's.
- createTestItem(imeX1, "X", "E", "en", 1, "en_US"),
- createTestItem(imeX2, "X", "E", "en", 1, "en_US"),
- createTestItem(imeX1, "X", "Z", "en", 4, "en_US"),
- createTestItem(imeX2, "X", "Z", "en", 4, "en_US"),
- createTestItem(imeX1, "X", "", "en", 7, "en_US"),
- createTestItem(imeX2, "X", "", "en", 7, "en_US"),
+ createTestItem(imeX1, "X", "E", "en", 1),
+ createTestItem(imeX2, "X", "E", "en", 1),
+ createTestItem(imeX1, "X", "Z", "en", 4),
+ createTestItem(imeX2, "X", "Z", "en", 4),
+ createTestItem(imeX1, "X", "", "en", 7),
+ createTestItem(imeX2, "X", "", "en", 7),
// Subtypes that has different language than the system's.
- createTestItem(imeX1, "X", "A", "hi_IN", 27, "en_US"),
- createTestItem(imeX2, "X", "A", "hi_IN", 27, "en_US"),
- createTestItem(imeX1, "X", "E", "ja", 2, "en_US"),
- createTestItem(imeX2, "X", "E", "ja", 2, "en_US"),
- createTestItem(imeX1, "X", "Z", "ja", 5, "en_US"),
- createTestItem(imeX2, "X", "Z", "ja", 5, "en_US"),
- createTestItem(imeX1, "X", "", "ja", 8, "en_US"),
- createTestItem(imeX2, "X", "", "ja", 8, "en_US"),
+ createTestItem(imeX1, "X", "A", "hi_IN", 27),
+ createTestItem(imeX2, "X", "A", "hi_IN", 27),
+ createTestItem(imeX1, "X", "E", "ja", 2),
+ createTestItem(imeX2, "X", "E", "ja", 2),
+ createTestItem(imeX1, "X", "Z", "ja", 5),
+ createTestItem(imeX2, "X", "Z", "ja", 5),
+ createTestItem(imeX1, "X", "", "ja", 8),
+ createTestItem(imeX2, "X", "", "ja", 8),
// Subtypes of IME "Y".
// Subtypes that has the same locale of the system's.
- createTestItem(imeY1, "Y", "E", "en_US", 9, "en_US"),
- createTestItem(imeY1, "Y", "Z", "en_US", 12, "en_US"),
- createTestItem(imeY1, "Y", "", "en_US", 15, "en_US"),
+ createTestItem(imeY1, "Y", "E", "en_US", 9),
+ createTestItem(imeY1, "Y", "Z", "en_US", 12),
+ createTestItem(imeY1, "Y", "", "en_US", 15),
// Subtypes that has the same language of the system's.
- createTestItem(imeY1, "Y", "E", "en", 10, "en_US"),
- createTestItem(imeY1, "Y", "Z", "en", 13, "en_US"),
- createTestItem(imeY1, "Y", "", "en", 16, "en_US"),
+ createTestItem(imeY1, "Y", "E", "en", 10),
+ createTestItem(imeY1, "Y", "Z", "en", 13),
+ createTestItem(imeY1, "Y", "", "en", 16),
// Subtypes that has different language than the system's.
- createTestItem(imeY1, "Y", "A", "hi_IN", 28, "en_US"),
- createTestItem(imeY1, "Y", "E", "ja", 11, "en_US"),
- createTestItem(imeY1, "Y", "Z", "ja", 14, "en_US"),
- createTestItem(imeY1, "Y", "", "ja", 17, "en_US"),
+ createTestItem(imeY1, "Y", "A", "hi_IN", 28),
+ createTestItem(imeY1, "Y", "E", "ja", 11),
+ createTestItem(imeY1, "Y", "Z", "ja", 14),
+ createTestItem(imeY1, "Y", "", "ja", 17),
// Subtypes of IME Z.
// Subtypes that has the same locale of the system's.
- createTestItem(imeZ1, "", "E", "en_US", 18, "en_US"),
- createTestItem(imeZ1, "", "Z", "en_US", 21, "en_US"),
- createTestItem(imeZ1, "", "", "en_US", 24, "en_US"),
+ createTestItem(imeZ1, "", "E", "en_US", 18),
+ createTestItem(imeZ1, "", "Z", "en_US", 21),
+ createTestItem(imeZ1, "", "", "en_US", 24),
// Subtypes that has the same language of the system's.
- createTestItem(imeZ1, "", "E", "en", 19, "en_US"),
- createTestItem(imeZ1, "", "Z", "en", 22, "en_US"),
- createTestItem(imeZ1, "", "", "en", 25, "en_US"),
+ createTestItem(imeZ1, "", "E", "en", 19),
+ createTestItem(imeZ1, "", "Z", "en", 22),
+ createTestItem(imeZ1, "", "", "en", 25),
// Subtypes that has different language than the system's.
- createTestItem(imeZ1, "", "A", "hi_IN", 29, "en_US"),
- createTestItem(imeZ1, "", "E", "ja", 20, "en_US"),
- createTestItem(imeZ1, "", "Z", "ja", 23, "en_US"),
- createTestItem(imeZ1, "", "", "ja", 26, "en_US"));
+ createTestItem(imeZ1, "", "A", "hi_IN", 29),
+ createTestItem(imeZ1, "", "E", "ja", 20),
+ createTestItem(imeZ1, "", "Z", "ja", 23),
+ createTestItem(imeZ1, "", "", "ja", 26));
// Ensure {@link java.lang.Comparable#compareTo} contracts are satisfied.
for (int i = 0; i < items.size(); ++i) {
@@ -449,10 +451,8 @@
{
// Following two items have the same priority.
- final ImeSubtypeListItem nonSystemLocale1 =
- createTestItem(imeX1, "X", "A", "ja_JP", 0, "en_US");
- final ImeSubtypeListItem nonSystemLocale2 =
- createTestItem(imeX1, "X", "A", "hi_IN", 1, "en_US");
+ final ImeSubtypeListItem nonSystemLocale1 = createTestItem(imeX1, "X", "A", "ja_JP", 0);
+ final ImeSubtypeListItem nonSystemLocale2 = createTestItem(imeX1, "X", "A", "hi_IN", 1);
assertEquals(0, nonSystemLocale1.compareTo(nonSystemLocale2));
assertEquals(0, nonSystemLocale2.compareTo(nonSystemLocale1));
// But those aren't equal to each other.
@@ -462,8 +462,8 @@
{
// Check if ComponentName is also taken into account when comparing two items.
- final ImeSubtypeListItem ime1 = createTestItem(imeX1, "X", "A", "ja_JP", 0, "en_US");
- final ImeSubtypeListItem ime2 = createTestItem(imeX2, "X", "A", "ja_JP", 0, "en_US");
+ final ImeSubtypeListItem ime1 = createTestItem(imeX1, "X", "A", "ja_JP", 0);
+ final ImeSubtypeListItem ime2 = createTestItem(imeX2, "X", "A", "ja_JP", 0);
assertTrue(ime1.compareTo(ime2) < 0);
assertTrue(ime2.compareTo(ime1) > 0);
// But those aren't equal to each other.
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index 4409051..30c384a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -94,4 +94,11 @@
verify(mDisplayOffloader).onBlockingScreenOn(eq(unblocker));
}
+
+ @Test
+ public void testUnblockScreenOn() {
+ mSession.cancelBlockScreenOn();
+
+ verify(mDisplayOffloader).cancelBlockScreenOn();
+ }
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 95f0b65..bb774ee 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -105,7 +105,6 @@
import java.util.List;
-
@SmallTest
@RunWith(AndroidJUnit4.class)
public final class DisplayPowerControllerTest {
@@ -1660,6 +1659,8 @@
int initState = Display.STATE_OFF;
mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+ when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
// start with OFF.
when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1673,6 +1674,7 @@
advanceTime(1); // Run updatePowerState
verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class));
+ verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
}
@Test
@@ -1680,6 +1682,8 @@
// set up.
int initState = Display.STATE_ON;
mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+ when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
// start with ON.
when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1692,7 +1696,78 @@
mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
advanceTime(1); // Run updatePowerState
- verify(mDisplayOffloadSession, never()).blockScreenOn(any(Runnable.class));
+ // No cancelBlockScreenOn call because we didn't block.
+ verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON)
+ @Test
+ public void testOffloadBlocker_turnON_thenOFF_cancelBlockScreenOnNotCalledIfUnblocked() {
+ // Set up.
+ int initState = Display.STATE_OFF;
+ mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+ mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+ when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
+ // Start with OFF.
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ dpr.policy = DisplayPowerRequest.POLICY_OFF;
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ // Go to ON.
+ dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mDisplayOffloadSession).blockScreenOn(argumentCaptor.capture());
+
+ // Unblocked
+ argumentCaptor.getValue().run();
+ advanceTime(1); // Run updatePowerState
+
+ // Go to OFF immediately
+ dpr.policy = DisplayPowerRequest.POLICY_OFF;
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ // No cancelBlockScreenOn call because we already unblocked
+ verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
+ }
+
+ @RequiresFlagsEnabled(Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON)
+ @Test
+ public void testOffloadBlocker_turnON_thenOFF_cancelBlockScreenOn() {
+ // Set up.
+ int initState = Display.STATE_OFF;
+ mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+ mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+ when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
+ // Start with OFF.
+ when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+ DisplayPowerRequest dpr = new DisplayPowerRequest();
+ dpr.policy = DisplayPowerRequest.POLICY_OFF;
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ // Go to ON.
+ dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ // We should call blockScreenOn
+ verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class));
+
+ // Go to OFF immediately
+ dpr.policy = DisplayPowerRequest.POLICY_OFF;
+ mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+ advanceTime(1); // Run updatePowerState
+
+ // We should call cancelBlockScreenOn
+ verify(mDisplayOffloadSession).cancelBlockScreenOn();
}
@Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 01ff35f..a7e0ebd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -33,7 +33,6 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
@@ -83,7 +82,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-
@SmallTest
@RunWith(AndroidJUnit4.class)
public class LocalDisplayAdapterTest {
@@ -126,7 +124,7 @@
private DisplayOffloadSessionImpl mDisplayOffloadSession;
- private DisplayOffloader mDisplayOffloader;
+ @Mock DisplayOffloader mDisplayOffloader;
private TestListener mListener = new TestListener();
@@ -1249,24 +1247,8 @@
}
private void initDisplayOffloadSession() {
- mDisplayOffloader = spy(new DisplayOffloader() {
- @Override
- public boolean startOffload() {
- return true;
- }
-
- @Override
- public void stopOffload() {}
-
- @Override
- public void onBlockingScreenOn(Runnable unblocker) {}
-
- @Override
- public boolean allowAutoBrightnessInDoze() {
- return true;
- }
- });
-
+ when(mDisplayOffloader.startOffload()).thenReturn(true);
+ when(mDisplayOffloader.allowAutoBrightnessInDoze()).thenReturn(true);
mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader,
mMockedDisplayPowerController);
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
index 498bffd..4e10b98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
@@ -192,7 +192,7 @@
}
@Test
- public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+ public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow() {
mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
int targetDisplayState = Display.STATE_DOZE;
boolean allowAutoBrightnessWhileDozing = true;
@@ -218,6 +218,32 @@
}
@Test
+ public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow_ScreenOff() {
+ mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+ int targetDisplayState = Display.STATE_OFF;
+ boolean allowAutoBrightnessWhileDozing = true;
+ int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+ int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+ float lastUserSetBrightness = 0.2f;
+ boolean userSetBrightnessChanged = true;
+ Settings.System.putFloat(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+ mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+ allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+ userSetBrightnessChanged);
+ verify(mAutomaticBrightnessController)
+ .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+ mBrightnessConfiguration,
+ lastUserSetBrightness,
+ userSetBrightnessChanged, /* adjustment */ 0.4f,
+ /* userChangedAutoBrightnessAdjustment= */ true, policy,
+ targetDisplayState, /* shouldResetShortTermModel */ true);
+ assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+ assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+ }
+
+ @Test
public void testAutoBrightnessState_DisplayIsOn() {
mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
int targetDisplayState = Display.STATE_ON;
@@ -245,6 +271,33 @@
}
@Test
+ public void testAutoBrightnessState_DisplayIsOn_PolicyIsDoze() {
+ mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+ int targetDisplayState = Display.STATE_ON;
+ boolean allowAutoBrightnessWhileDozing = false;
+ int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+ float lastUserSetBrightness = 0.2f;
+ boolean userSetBrightnessChanged = true;
+ int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+ float pendingBrightnessAdjustment = 0.1f;
+ Settings.System.putFloat(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+ mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+ allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+ userSetBrightnessChanged);
+ verify(mAutomaticBrightnessController)
+ .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+ mBrightnessConfiguration,
+ lastUserSetBrightness,
+ userSetBrightnessChanged, pendingBrightnessAdjustment,
+ /* userChangedAutoBrightnessAdjustment= */ true, policy, targetDisplayState,
+ /* shouldResetShortTermModel */ true);
+ assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+ assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+ }
+
+ @Test
public void accommodateUserBrightnessChangesWorksAsExpected() {
// Verify the state if automaticBrightnessController is configured.
assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index 1d04baa..e16377e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -202,7 +202,7 @@
}
@Test
- public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+ public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow() {
mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
int targetDisplayState = Display.STATE_DOZE;
boolean allowAutoBrightnessWhileDozing = true;
@@ -228,6 +228,32 @@
}
@Test
+ public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow_ScreenOff() {
+ mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+ int targetDisplayState = Display.STATE_OFF;
+ boolean allowAutoBrightnessWhileDozing = true;
+ int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+ int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+ float lastUserSetBrightness = 0.2f;
+ boolean userSetBrightnessChanged = true;
+ Settings.System.putFloat(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+ mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+ allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+ userSetBrightnessChanged);
+ verify(mAutomaticBrightnessController)
+ .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+ mBrightnessConfiguration,
+ lastUserSetBrightness,
+ userSetBrightnessChanged, /* adjustment */ 0.4f,
+ /* userChangedAutoBrightnessAdjustment= */ true, policy,
+ targetDisplayState, /* shouldResetShortTermModel */ true);
+ assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+ assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+ }
+
+ @Test
public void testAutoBrightnessState_DisplayIsOn() {
mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
int targetDisplayState = Display.STATE_ON;
@@ -255,6 +281,33 @@
}
@Test
+ public void testAutoBrightnessState_DisplayIsOn_PolicyIsDoze() {
+ mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+ int targetDisplayState = Display.STATE_ON;
+ boolean allowAutoBrightnessWhileDozing = false;
+ int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+ float lastUserSetBrightness = 0.2f;
+ boolean userSetBrightnessChanged = true;
+ int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+ float pendingBrightnessAdjustment = 0.1f;
+ Settings.System.putFloat(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+ mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+ mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+ allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+ userSetBrightnessChanged);
+ verify(mAutomaticBrightnessController)
+ .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+ mBrightnessConfiguration,
+ lastUserSetBrightness,
+ userSetBrightnessChanged, pendingBrightnessAdjustment,
+ /* userChangedAutoBrightnessAdjustment= */ true, policy, targetDisplayState,
+ /* shouldResetShortTermModel */ true);
+ assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+ assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+ }
+
+ @Test
public void testAutoBrightnessState_modeSwitch() {
// Setup the test
when(mDisplayManagerFlags.areAutoBrightnessModesEnabled()).thenReturn(true);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 4e8c755..9884085 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -25,6 +25,8 @@
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
import static com.android.server.accessibility.AccessibilityManagerService.ACTION_LAUNCH_HEARING_DEVICES_DIALOG;
import static com.android.window.flags.Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER;
@@ -1082,7 +1084,7 @@
mA11yms.enableShortcutsForTargets(
/* enable= */ true,
- UserShortcutType.HARDWARE,
+ HARDWARE,
List.of(target),
mA11yms.getCurrentUserIdLocked());
mTestableLooper.processAllMessages();
@@ -1346,14 +1348,14 @@
mA11yms.enableShortcutsForTargets(
/* enable= */ true,
- UserShortcutType.HARDWARE,
+ HARDWARE,
List.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString()),
mA11yms.getCurrentUserIdLocked());
mTestableLooper.processAllMessages();
assertThat(
ShortcutUtils.isComponentIdExistingInSettings(
- mTestableContext, ShortcutConstants.UserShortcutType.HARDWARE,
+ mTestableContext, HARDWARE,
TARGET_STANDARD_A11Y_SERVICE.flattenToString())
).isTrue();
}
@@ -1367,7 +1369,7 @@
mA11yms.enableShortcutsForTargets(
/* enable= */ false,
- UserShortcutType.HARDWARE,
+ HARDWARE,
List.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString()),
mA11yms.getCurrentUserIdLocked());
mTestableLooper.processAllMessages();
@@ -1375,7 +1377,7 @@
assertThat(
ShortcutUtils.isComponentIdExistingInSettings(
mTestableContext,
- ShortcutConstants.UserShortcutType.HARDWARE,
+ HARDWARE,
TARGET_STANDARD_A11Y_SERVICE.flattenToString()))
.isFalse();
}
@@ -1390,14 +1392,14 @@
mA11yms.enableShortcutsForTargets(
/* enable= */ true,
- UserShortcutType.QUICK_SETTINGS,
+ QUICK_SETTINGS,
List.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()),
mA11yms.getCurrentUserIdLocked());
mTestableLooper.processAllMessages();
assertThat(
ShortcutUtils.isComponentIdExistingInSettings(
- mTestableContext, UserShortcutType.QUICK_SETTINGS,
+ mTestableContext, QUICK_SETTINGS,
TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())
).isTrue();
verify(mStatusBarManagerInternal)
@@ -1417,14 +1419,14 @@
mA11yms.enableShortcutsForTargets(
/* enable= */ false,
- UserShortcutType.QUICK_SETTINGS,
+ QUICK_SETTINGS,
List.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()),
mA11yms.getCurrentUserIdLocked());
mTestableLooper.processAllMessages();
assertThat(
ShortcutUtils.isComponentIdExistingInSettings(
- mTestableContext, UserShortcutType.QUICK_SETTINGS,
+ mTestableContext, QUICK_SETTINGS,
TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())
).isFalse();
verify(mStatusBarManagerInternal)
@@ -1614,44 +1616,49 @@
@Test
@EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
- public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() {
+ public void restoreShortcutTargets_qs_a11yQsTargetsRestored() {
String daltonizerTile =
AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
String colorInversionTile =
AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
final AccessibilityUserState userState = new AccessibilityUserState(
UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
- userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+ userState.updateShortcutTargetsLocked(Set.of(daltonizerTile), QUICK_SETTINGS);
mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
broadcastSettingRestored(
- Settings.Secure.ACCESSIBILITY_QS_TARGETS,
- /*previousValue=*/null,
+ ShortcutUtils.convertToKey(QUICK_SETTINGS),
/*newValue=*/colorInversionTile);
- assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
- .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
+ Set<String> expected = Set.of(daltonizerTile, colorInversionTile);
+ assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
+ .containsExactlyElementsIn(expected);
+ assertThat(userState.getShortcutTargetsLocked(QUICK_SETTINGS))
+ .containsExactlyElementsIn(expected);
}
@Test
@DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
- public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() {
+ public void restoreShortcutTargets_qs_a11yQsTargetsNotRestored() {
String daltonizerTile =
AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
String colorInversionTile =
AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
final AccessibilityUserState userState = new AccessibilityUserState(
UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
- userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+ userState.updateShortcutTargetsLocked(Set.of(daltonizerTile), QUICK_SETTINGS);
+ putShortcutSettingForUser(QUICK_SETTINGS, daltonizerTile, userState.mUserId);
mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
broadcastSettingRestored(
- Settings.Secure.ACCESSIBILITY_QS_TARGETS,
- /*previousValue=*/null,
+ ShortcutUtils.convertToKey(QUICK_SETTINGS),
/*newValue=*/colorInversionTile);
- assertThat(userState.getA11yQsTargets())
- .containsExactlyElementsIn(Set.of(daltonizerTile));
+ Set<String> expected = Set.of(daltonizerTile);
+ assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
+ .containsExactlyElementsIn(expected);
+ assertThat(userState.getShortcutTargetsLocked(QUICK_SETTINGS))
+ .containsExactlyElementsIn(expected);
}
@Test
@@ -1717,27 +1724,26 @@
@Test
@EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE)
- public void restoreA11yShortcutTargetService_targetsMerged() {
+ public void restoreShortcutTargets_hardware_targetsMerged() {
+ mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
final String servicePrevious = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
final String otherPrevious = TARGET_MAGNIFICATION;
- final String combinedPrevious = String.join(":", servicePrevious, otherPrevious);
final String serviceRestored = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
final AccessibilityUserState userState = new AccessibilityUserState(
UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
setupShortcutTargetServices(userState);
+ mA11yms.enableShortcutsForTargets(
+ true, HARDWARE, List.of(servicePrevious, otherPrevious), userState.mUserId);
broadcastSettingRestored(
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
- /*previousValue=*/combinedPrevious,
+ ShortcutUtils.convertToKey(HARDWARE),
/*newValue=*/serviceRestored);
final Set<String> expected = Set.of(servicePrevious, otherPrevious, serviceRestored);
- assertThat(readStringsFromSetting(
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+ assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
.containsExactlyElementsIn(expected);
- assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
- .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+ assertThat(userState.getShortcutTargetsLocked(HARDWARE))
.containsExactlyElementsIn(expected);
}
@@ -1745,7 +1751,7 @@
@EnableFlags({
android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
- public void restoreA11yShortcutTargetService_alreadyHadDefaultService_doesNotClear() {
+ public void restoreShortcutTargets_hardware_alreadyHadDefaultService_doesNotClear() {
final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
mTestableContext.getOrCreateTestableResources().addOverride(
R.string.config_defaultAccessibilityService, serviceDefault);
@@ -1754,17 +1760,18 @@
mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
setupShortcutTargetServices(userState);
+ // default is present in userState & setting, so it's not cleared
+ putShortcutSettingForUser(HARDWARE, serviceDefault, UserHandle.USER_SYSTEM);
+ userState.updateShortcutTargetsLocked(Set.of(serviceDefault), HARDWARE);
+
broadcastSettingRestored(
Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
- /*previousValue=*/serviceDefault,
/*newValue=*/serviceDefault);
final Set<String> expected = Set.of(serviceDefault);
- assertThat(readStringsFromSetting(
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+ assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
.containsExactlyElementsIn(expected);
- assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
- .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+ assertThat(userState.getShortcutTargetsLocked(HARDWARE))
.containsExactlyElementsIn(expected);
}
@@ -1772,7 +1779,7 @@
@EnableFlags({
android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
- public void restoreA11yShortcutTargetService_didNotHaveDefaultService_clearsDefaultService() {
+ public void restoreShortcutTargets_hardware_didNotHaveDefaultService_clearsDefaultService() {
final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
// Restored value from the broadcast contains both default and non-default service.
@@ -1784,18 +1791,45 @@
mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
setupShortcutTargetServices(userState);
- broadcastSettingRestored(
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
- /*previousValue=*/null,
+ broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
/*newValue=*/combinedRestored);
// The default service is cleared from the final restored value.
final Set<String> expected = Set.of(serviceRestored);
- assertThat(readStringsFromSetting(
- Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+ assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
.containsExactlyElementsIn(expected);
- assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
- .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+ assertThat(userState.getShortcutTargetsLocked(HARDWARE))
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ @EnableFlags({
+ android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
+ Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
+ public void restoreShortcutTargets_hardware_nullSetting_clearsDefaultService() {
+ final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
+ final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
+ // Restored value from the broadcast contains both default and non-default service.
+ final String combinedRestored = String.join(":", serviceDefault, serviceRestored);
+ mTestableContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultAccessibilityService, serviceDefault);
+ final AccessibilityUserState userState = new AccessibilityUserState(
+ UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+ mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+ setupShortcutTargetServices(userState);
+
+ // UserState has default, but setting is null (this emulates a typical scenario in SUW).
+ userState.updateShortcutTargetsLocked(Set.of(serviceDefault), HARDWARE);
+ putShortcutSettingForUser(HARDWARE, null, UserHandle.USER_SYSTEM);
+
+ broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
+ /*newValue=*/combinedRestored);
+
+ // The default service is cleared from the final restored value.
+ final Set<String> expected = Set.of(serviceRestored);
+ assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
+ .containsExactlyElementsIn(expected);
+ assertThat(userState.getShortcutTargetsLocked(HARDWARE))
.containsExactlyElementsIn(expected);
}
@@ -1806,11 +1840,10 @@
return result;
}
- private void broadcastSettingRestored(String setting, String previousValue, String newValue) {
+ private void broadcastSettingRestored(String setting, String newValue) {
Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
.putExtra(Intent.EXTRA_SETTING_NAME, setting)
- .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, previousValue)
.putExtra(Intent.EXTRA_SETTING_NEW_VALUE, newValue);
sendBroadcastToAccessibilityManagerService(intent);
mTestableLooper.processAllMessages();
@@ -1952,4 +1985,13 @@
private static boolean isSameCurrentUser(AccessibilityManagerService service, Context context) {
return service.getCurrentUserIdLocked() == context.getUserId();
}
+
+ private void putShortcutSettingForUser(@UserShortcutType int shortcutType,
+ String shortcutValue, int userId) {
+ Settings.Secure.putStringForUser(
+ mTestableContext.getContentResolver(),
+ ShortcutUtils.convertToKey(shortcutType),
+ shortcutValue,
+ userId);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index b269beb9..9fad14d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -28,6 +28,7 @@
import static android.view.accessibility.AccessibilityManager.STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED;
import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
import static com.google.common.truth.Truth.assertThat;
@@ -429,20 +430,20 @@
}
@Test
- public void updateA11yQsTargetLocked_valueUpdated() {
+ public void updateShortcutTargetsLocked_quickSettings_valueUpdated() {
Set<String> newTargets = Set.of(
AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(),
AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString()
);
- mUserState.updateA11yQsTargetLocked(newTargets);
+ mUserState.updateShortcutTargetsLocked(newTargets, QUICK_SETTINGS);
assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets);
}
@Test
public void getA11yQsTargets_returnsCopiedData() {
- updateA11yQsTargetLocked_valueUpdated();
+ updateShortcutTargetsLocked_quickSettings_valueUpdated();
Set<String> targets = mUserState.getA11yQsTargets();
targets.clear();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index c1d7afb..c48d745 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -14335,6 +14335,29 @@
@Test
@EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN)
+ public void enqueueNotification_directlyThroughRunnable_populatesAllowlistToken() {
+ Notification receivedWithoutParceling = new Notification.Builder(mContext, TEST_CHANNEL_ID)
+ .setContentIntent(createPendingIntent("content"))
+ .build();
+ NotificationRecord record = new NotificationRecord(
+ mContext,
+ new StatusBarNotification(mPkg, mPkg, 1, "tag", mUid, 44, receivedWithoutParceling,
+ mUser, "groupKey", 0),
+ mTestNotificationChannel);
+ assertThat(record.getNotification().getAllowlistToken()).isNull();
+
+ mWorkerHandler.post(
+ mService.new EnqueueNotificationRunnable(mUserId, record, false, false,
+ mPostNotificationTrackerFactory.newTracker(null)));
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken())
+ .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN);
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN)
public void enqueueNotification_rejectsOtherToken() throws RemoteException {
Notification sent = new Notification.Builder(mContext, TEST_CHANNEL_ID)
.setContentIntent(createPendingIntent("content"))
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index d143297..13d52ea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3098,6 +3098,30 @@
}
@Test
+ public void testOnStartingWindowDrawn() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ // The task-has-been-visible should not affect the decision of making transition ready.
+ activity.getTask().setHasBeenVisible(true);
+ activity.detachFromProcess();
+ activity.mStartingData = mock(StartingData.class);
+ registerTestTransitionPlayer();
+ final Transition transition = activity.mTransitionController.requestTransitionIfNeeded(
+ WindowManager.TRANSIT_OPEN, 0 /* flags */, null /* trigger */, mDisplayContent);
+ activity.onStartingWindowDrawn();
+ assertTrue(activity.mStartingData.mIsDisplayed);
+ // The transition can be ready by the starting window of a visible-requested activity
+ // without a running process.
+ assertTrue(transition.allReady());
+
+ // If other event makes the transition unready, the reentrant of onStartingWindowDrawn
+ // should not replace the readiness again.
+ transition.setReady(mDisplayContent, false);
+ activity.onStartingWindowDrawn();
+ assertFalse(transition.allReady());
+ }
+
+
+ @Test
public void testCloseToSquareFixedOrientation() {
if (Flags.insetsDecoupledConfiguration()) {
// No test needed as decor insets no longer affects orientation.
@@ -3333,14 +3357,8 @@
// to client if the app didn't request IME visible.
assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
- if (Flags.bundleClientTransactionFlag()) {
- verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
- isA(WindowStateResizeItem.class));
- } else {
- verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
- insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
- anyBoolean(), any());
- }
+ verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
+ isA(WindowStateResizeItem.class));
assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
new file mode 100644
index 0000000..2260999
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -0,0 +1,570 @@
+/*
+ * 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.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatOrientationPolicy}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:AppCompatOrientationPolicyTest
+ */
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatOrientationPolicyTest extends WindowTestsBase {
+
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @Test
+ public void testOverrideOrientationIfNeeded_mapInvokedOnRequest() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.overrideOrientationIfNeeded(SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOrientationRequestMapped();
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_isUnchanged() {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutSystem_returnsUser() {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+ robot.configureIsUserAppAspectRatioFullscreenEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutUser_returnsUser() {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+ robot.configureIsUserAppAspectRatioFullscreenEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
+ throws Exception {
+ runTestScenarioWithActivity((robot) -> {
+ robot.configureSetIgnoreOrientationRequest(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_isUnchanged() {
+ runTestScenario((robot) -> {
+ robot.prepareIsUserAppAspectRatioSettingsEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+ public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+ public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_isUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+ public void testOverrideOrientationIfNeeded_reverseLandscape_portraitOrUndefined_isUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+ public void testOverrideOrientationIfNeeded_reverseLandscape_Landscape_getsReverseLandscape() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* expected */ SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_IsUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_NOSENSOR,
+ /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_NOSENSOR,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_propertyIsFalse_isUnchanged()
+ throws Exception {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+
+ robot.createActivityWithComponent();
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraNotActive_isUnchanged() {
+ runTestScenario((robot) -> {
+ robot.configureIsCameraCompatTreatmentEnabled(true);
+ robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.prepareIsTopActivityEligibleForOrientationOverride(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
+ runTestScenario((robot) -> {
+ robot.configureIsCameraCompatTreatmentEnabled(true);
+ robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.prepareIsTopActivityEligibleForOrientationOverride(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_fullscreenOverride_cameraActivity_unchanged() {
+ runTestScenario((robot) -> {
+ robot.configureIsCameraCompatTreatmentEnabled(true);
+ robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.configureIsTopActivityCameraActive(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(false);
+
+ robot.checkOverrideOrientationIsNot(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* notExpected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_userFullScreenOverrideOverSystem_returnsUser() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_respectOrientationReqOverUserFullScreenAndSystem() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(false);
+
+ robot.checkOverrideOrientationIsNot(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* notExpected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userFullScreenOverrideDisabled_returnsUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userAspectRatioApplied_unspecifiedOverridden() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserMinAspectRatioOverride(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LOCKED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* expected */ SCREEN_ORIENTATION_LANDSCAPE);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userAspectRatioNotApplied_isUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+
+ /**
+ * Runs a test scenario with an existing activity providing a Robot.
+ */
+ void runTestScenarioWithActivity(@NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+ runTestScenario(/* withActivity */ true, consumer);
+ }
+
+ /**
+ * Runs a test scenario without an existing activity providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+ runTestScenario(/* withActivity */ false, consumer);
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(boolean withActivity,
+ @NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+ spyOn(mWm.mLetterboxConfiguration);
+ final OrientationPolicyRobotTest robot =
+ new OrientationPolicyRobotTest(mWm, mAtm, mSupervisor, withActivity);
+ consumer.accept(robot);
+ }
+
+ private static class OrientationPolicyRobotTest {
+
+ @NonNull
+ private final ActivityTaskManagerService mAtm;
+ @NonNull
+ private final WindowManagerService mWm;
+ @NonNull
+ private final LetterboxConfiguration mLetterboxConfiguration;
+ @NonNull
+ private final TestComponentStack<ActivityRecord> mActivityStack;
+ @NonNull
+ private final TestComponentStack<Task> mTaskStack;
+
+ @NonNull
+ private final ActivityTaskSupervisor mSupervisor;
+
+ OrientationPolicyRobotTest(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor,
+ boolean withActivity) {
+ mAtm = atm;
+ mWm = wm;
+ spyOn(mWm);
+ mSupervisor = supervisor;
+ mActivityStack = new TestComponentStack<>();
+ mTaskStack = new TestComponentStack<>();
+ mLetterboxConfiguration = mWm.mLetterboxConfiguration;
+ if (withActivity) {
+ createActivityWithComponent();
+ }
+ }
+
+ void configureSetIgnoreOrientationRequest(boolean enabled) {
+ mActivityStack.top().mDisplayContent.setIgnoreOrientationRequest(enabled);
+ }
+
+ void configureIsUserAppAspectRatioFullscreenEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled();
+ }
+
+ void configureIsCameraCompatTreatmentEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+ }
+
+ void configureIsCameraCompatTreatmentEnabledAtBuildTime(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabledAtBuildTime();
+ }
+
+ void prepareGetUserMinAspectRatioOverrideCode(int orientation) {
+ spyOn(mActivityStack.top().mLetterboxUiController);
+ doReturn(orientation).when(mActivityStack.top()
+ .mLetterboxUiController).getUserMinAspectRatioOverrideCode();
+ }
+
+ void prepareShouldApplyUserFullscreenOverride(boolean enabled) {
+ spyOn(mActivityStack.top().mLetterboxUiController);
+ doReturn(enabled).when(mActivityStack.top()
+ .mLetterboxUiController).shouldApplyUserFullscreenOverride();
+ }
+
+ void prepareShouldApplyUserMinAspectRatioOverride(boolean enabled) {
+ spyOn(mActivityStack.top().mLetterboxUiController);
+ doReturn(enabled).when(mActivityStack.top()
+ .mLetterboxUiController).shouldApplyUserMinAspectRatioOverride();
+ }
+
+ void prepareIsUserAppAspectRatioSettingsEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
+ }
+
+ void prepareIsTopActivityEligibleForOrientationOverride(boolean enabled) {
+ final DisplayRotationCompatPolicy displayPolicy =
+ mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+ spyOn(displayPolicy);
+ doReturn(enabled).when(displayPolicy)
+ .isActivityEligibleForOrientationOverride(eq(mActivityStack.top()));
+ }
+
+ void configureIsTopActivityCameraActive(boolean enabled) {
+ final DisplayRotationCompatPolicy displayPolicy =
+ mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+ spyOn(displayPolicy);
+ doReturn(enabled).when(displayPolicy)
+ .isCameraActive(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true));
+ }
+
+ void disableProperty(@NonNull String propertyName) {
+ setPropertyValue(propertyName, /* enabled */ false);
+ }
+
+ int overrideOrientationIfNeeded(@ActivityInfo.ScreenOrientation int candidate) {
+ return mActivityStack.top().mAppCompatController.getOrientationPolicy()
+ .overrideOrientationIfNeeded(candidate);
+ }
+
+ void checkOrientationRequestMapped() {
+ verify(mWm).mapOrientationRequest(SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ void checkOverrideOrientation(@ActivityInfo.ScreenOrientation int candidate,
+ @ActivityInfo.ScreenOrientation int expected) {
+ Assert.assertEquals(expected, overrideOrientationIfNeeded(candidate));
+ }
+
+ void checkOverrideOrientationIsNot(@ActivityInfo.ScreenOrientation int candidate,
+ @ActivityInfo.ScreenOrientation int notExpected) {
+ Assert.assertNotEquals(notExpected, overrideOrientationIfNeeded(candidate));
+ }
+
+ private void createActivityWithComponent() {
+ if (mTaskStack.isEmpty()) {
+ final DisplayContent displayContent = new TestDisplayContent
+ .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+ mTaskStack.push(task);
+ }
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setOnTop(true)
+ .setTask(mTaskStack.top())
+ // Set the component to be that of the test class in order
+ // to enable compat changes
+ .setComponent(ComponentName.createRelative(mAtm.mContext,
+ com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+ .build();
+ mActivityStack.push(activity);
+ }
+
+ private void createActivityWithComponentInNewTask() {
+ final DisplayContent displayContent = new TestDisplayContent
+ .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setOnTop(true)
+ .setTask(task)
+ // Set the component to be that of the test class in order
+ // to enable compat changes
+ .setComponent(ComponentName.createRelative(mAtm.mContext,
+ com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+ .build();
+ mTaskStack.push(task);
+ mActivityStack.push(activity);
+ }
+
+ private void setPropertyValue(@NonNull String propertyName, boolean enabled) {
+ PackageManager.Property property = new PackageManager.Property(propertyName,
+ /* value */ enabled, /* packageName */ "",
+ /* className */ "");
+ PackageManager pm = mWm.mContext.getPackageManager();
+ spyOn(pm);
+ try {
+ doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+ } catch (PackageManager.NameNotFoundException e) {
+ fail(e.getLocalizedMessage());
+ }
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
index b21eca7..2bda950 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ClientLifecycleManagerTests.java
@@ -19,7 +19,6 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.window.flags.Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -106,31 +105,7 @@
}
@Test
- public void testScheduleTransactionItem_notBundle() throws RemoteException {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- // Use non binder client to get non-recycled ClientTransaction.
- mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mTransactionItem);
-
- verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
- ClientTransaction transaction = mTransactionCaptor.getValue();
- assertEquals(1, transaction.getCallbacks().size());
- assertEquals(mTransactionItem, transaction.getCallbacks().get(0));
- assertNull(transaction.getLifecycleStateRequest());
- assertNull(transaction.getTransactionItems());
-
- clearInvocations(mLifecycleManager);
- mLifecycleManager.scheduleTransactionItem(mNonBinderClient, mLifecycleItem);
-
- verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
- transaction = mTransactionCaptor.getValue();
- assertNull(transaction.getCallbacks());
- assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest());
- }
-
- @Test
public void testScheduleTransactionItem() throws RemoteException {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
spyOn(mWms.mWindowPlacerLocked);
doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
@@ -176,23 +151,7 @@
}
@Test
- public void testScheduleTransactionAndLifecycleItems_notBundle() throws RemoteException {
- mSetFlagsRule.disableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- // Use non binder client to get non-recycled ClientTransaction.
- mLifecycleManager.scheduleTransactionAndLifecycleItems(mNonBinderClient, mTransactionItem,
- mLifecycleItem);
-
- verify(mLifecycleManager).scheduleTransaction(mTransactionCaptor.capture());
- final ClientTransaction transaction = mTransactionCaptor.getValue();
- assertEquals(1, transaction.getCallbacks().size());
- assertEquals(mTransactionItem, transaction.getCallbacks().get(0));
- assertEquals(mLifecycleItem, transaction.getLifecycleStateRequest());
- }
-
- @Test
public void testScheduleTransactionAndLifecycleItems() throws RemoteException {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
spyOn(mWms.mWindowPlacerLocked);
doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
@@ -216,7 +175,6 @@
@Test
public void testScheduleTransactionAndLifecycleItems_shouldDispatchImmediately()
throws RemoteException {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
spyOn(mWms.mWindowPlacerLocked);
doReturn(true).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
@@ -230,8 +188,6 @@
@Test
public void testDispatchPendingTransactions() throws RemoteException {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
mLifecycleManager.mPendingTransactions.put(mClientBinder, mTransaction);
mLifecycleManager.dispatchPendingTransactions();
@@ -243,7 +199,6 @@
@Test
public void testLayoutDeferred() throws RemoteException {
- mSetFlagsRule.enableFlags(FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
spyOn(mWms.mWindowPlacerLocked);
doReturn(false).when(mWms.mWindowPlacerLocked).isInLayout();
doReturn(false).when(mWms.mWindowPlacerLocked).isTraversalScheduled();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index b1200bcd..bdd45c6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -18,27 +18,14 @@
import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
-import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
-import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
-import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
-import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -49,7 +36,6 @@
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
@@ -535,322 +521,6 @@
return mainWindow;
}
- // overrideOrientationIfNeeded
-
- @Test
- public void testOverrideOrientationIfNeeded_mapInvokedOnRequest() throws Exception {
- mController = new LetterboxUiController(mWm, mActivity);
- spyOn(mWm);
-
- mController.overrideOrientationIfNeeded(SCREEN_ORIENTATION_PORTRAIT);
-
- verify(mWm).mapOrientationRequest(SCREEN_ORIENTATION_PORTRAIT);
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser()
- throws Exception {
- mDisplayContent.setIgnoreOrientationRequest(true);
- assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_returnsUnchanged()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- mDisplayContent.setIgnoreOrientationRequest(true);
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutSystem_returnsUser()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
- prepareActivityThatShouldApplyUserFullscreenOverride();
-
- // fullscreen override still applied
- assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutUser_returnsUser()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
- /* value */ false);
- prepareActivityThatShouldApplyUserFullscreenOverride();
-
- // fullscreen override still applied
- assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
- throws Exception {
- mDisplayContent.setIgnoreOrientationRequest(false);
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_returnsUnchanged()
- throws Exception {
- doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
- mActivity = setUpActivityWithComponent();
- spyOn(mActivity.mLetterboxUiController);
- doReturn(USER_MIN_ASPECT_RATIO_3_2).when(mActivity.mLetterboxUiController)
- .getUserMinAspectRatioOverrideCode();
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
- public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait()
- throws Exception {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
- public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
- assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
- public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_returnsUnchanged() {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
- public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationPortraitOrUndefined_returnsUnchanged() {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
- public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationLandscape_returnsReverseLandscape() {
- assertEquals(SCREEN_ORIENTATION_REVERSE_LANDSCAPE, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_LANDSCAPE));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
- public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_returnsUnchanged() {
- assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_NOSENSOR));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_NOSENSOR));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
- assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
- public void testOverrideOrientationIfNeeded_propertyIsFalse_returnsUnchanged()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
-
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
- OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
- public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(false).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isActivityEligibleForOrientationOverride(eq(mActivity));
-
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
- OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
- public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isActivityEligibleForOrientationOverride(eq(mActivity));
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() {
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController)
- .shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(true);
-
- assertEquals(SCREEN_ORIENTATION_USER, mActivity.mAppCompatController
- .getOrientationPolicy()
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userFullscreenOverride_cameraActivity_noChange() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- spyOn(mController);
- doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isCameraActive(mActivity, /* mustBeFullscreen= */ true);
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_systemFullscreenOverride_cameraActivity_noChange() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- spyOn(mController);
- doReturn(true).when(mController).isSystemOverrideToFullscreenEnabled();
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isCameraActive(mActivity, /* mustBeFullscreen= */ true);
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() {
- spyOn(mController);
- doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(false);
-
- assertNotEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_userFullScreenOverrideOverSystem_returnsUser() {
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController)
- .shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(true);
-
- assertEquals(SCREEN_ORIENTATION_USER, mActivity.mAppCompatController
- .getOrientationPolicy()
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_respectOrientationReqOverUserFullScreenAndSystem() {
- spyOn(mController);
- doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(false);
-
- assertNotEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userFullScreenOverrideDisabled_returnsUnchanged() {
- spyOn(mController);
- doReturn(false).when(mController).shouldApplyUserFullscreenOverride();
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userAspectRatioApplied_unspecifiedOverridden() {
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController)
- .shouldApplyUserMinAspectRatioOverride();
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_LOCKED));
-
- // unchanged if orientation is specified
- assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userAspectRatioNotApplied_returnsUnchanged() {
- spyOn(mController);
- doReturn(false).when(mController).shouldApplyUserMinAspectRatioOverride();
-
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
// shouldApplyUser...Override
@Test
public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() throws Exception {
@@ -1528,12 +1198,6 @@
mDisplayContent.setIgnoreOrientationRequest(true);
}
- private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- mController.setRelaunchingAfterRequestedOrientationChanged(true);
- }
-
private ActivityRecord setUpActivityWithComponent() {
mDisplayContent = new TestDisplayContent
.Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
index 6c5f975..1c32980 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SyncEngineTests.java
@@ -33,6 +33,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -417,6 +418,22 @@
}
@Test
+ public void testSkipPrepareSync() {
+ final TestWindowContainer wc = new TestWindowContainer(mWm, true /* waiter */);
+ wc.mSkipPrepareSync = true;
+ final BLASTSyncEngine bse = createTestBLASTSyncEngine();
+ final BLASTSyncEngine.SyncGroup syncGroup = bse.prepareSyncSet(
+ mock(BLASTSyncEngine.TransactionReadyListener.class), "test");
+ bse.startSyncSet(syncGroup);
+ bse.addToSyncSet(syncGroup.mSyncId, wc);
+ assertEquals(SYNC_STATE_NONE, wc.mSyncState);
+ // If the implementation of prepareSync doesn't set sync state, the sync group should also
+ // be empty.
+ assertNull(wc.mSyncGroup);
+ assertTrue(wc.isSyncFinished(syncGroup));
+ }
+
+ @Test
public void testNonBlastMethod() {
mAppWindow = createWindow(null, TYPE_BASE_APPLICATION, "mAppWindow");
@@ -694,6 +711,7 @@
final boolean mWaiter;
boolean mVisibleRequested = true;
boolean mFillsParent = false;
+ boolean mSkipPrepareSync = false;
TestWindowContainer(WindowManagerService wms, boolean waiter) {
super(wms);
@@ -703,6 +721,9 @@
@Override
boolean prepareSync() {
+ if (mSkipPrepareSync) {
+ return false;
+ }
if (!super.prepareSync()) {
return false;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 9670a9a..a71b81e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -26,8 +26,8 @@
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK;
@@ -220,30 +220,7 @@
}
@Test
- public void testOnTaskFragmentAppeared_throughTaskFragmentOrganizer() throws RemoteException {
- mSetFlagsRule.disableFlags(Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
-
- // No-op when the TaskFragment is not attached.
- mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
- mController.dispatchPendingEvents();
-
- verify(mOrganizer, never()).onTransactionReady(any());
- verify(mAppThread, never()).scheduleTaskFragmentTransaction(any(), any());
-
- // Send callback when the TaskFragment is attached.
- setupMockParent(mTaskFragment, mTask);
-
- mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
- mController.dispatchPendingEvents();
-
- assertTaskFragmentParentInfoChangedTransaction(mTask);
- assertTaskFragmentAppearedTransaction(false /* hasSurfaceControl */);
- verify(mAppThread, never()).scheduleTaskFragmentTransaction(any(), any());
- }
-
- @Test
public void testOnTaskFragmentAppeared_throughApplicationThread() throws RemoteException {
- mSetFlagsRule.enableFlags(Flags.FLAG_BUNDLE_CLIENT_TRANSACTION_FLAG);
// Re-register the organizer in case the flag was disabled during setup.
mController.unregisterOrganizer(mIOrganizer);
registerTaskFragmentOrganizer(mIOrganizer);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index a8a9017..ba33eab 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -33,6 +33,7 @@
import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
@@ -136,6 +137,7 @@
// The obfuscated packages to tokens mappings file
private final File mPackageMappingsFile;
// Holds all of the data related to the obfuscated packages and their token mappings.
+ @GuardedBy("mLock")
final PackagesTokenData mPackagesTokenData = new PackagesTokenData();
/**
@@ -771,27 +773,30 @@
* all of the stats at once has an amortized cost for future calls.
*/
void filterStats(IntervalStats stats) {
- if (mPackagesTokenData.removedPackagesMap.isEmpty()) {
- return;
- }
- final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;
-
- // filter out package usage stats
- final int removedPackagesSize = removedPackagesMap.size();
- for (int i = 0; i < removedPackagesSize; i++) {
- final String removedPackage = removedPackagesMap.keyAt(i);
- final UsageStats usageStats = stats.packageStats.get(removedPackage);
- if (usageStats != null && usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
- stats.packageStats.remove(removedPackage);
+ synchronized (mLock) {
+ if (mPackagesTokenData.removedPackagesMap.isEmpty()) {
+ return;
}
- }
+ final ArrayMap<String, Long> removedPackagesMap = mPackagesTokenData.removedPackagesMap;
- // filter out events
- for (int i = stats.events.size() - 1; i >= 0; i--) {
- final UsageEvents.Event event = stats.events.get(i);
- final Long timeRemoved = removedPackagesMap.get(event.mPackage);
- if (timeRemoved != null && timeRemoved > event.mTimeStamp) {
- stats.events.remove(i);
+ // filter out package usage stats
+ final int removedPackagesSize = removedPackagesMap.size();
+ for (int i = 0; i < removedPackagesSize; i++) {
+ final String removedPackage = removedPackagesMap.keyAt(i);
+ final UsageStats usageStats = stats.packageStats.get(removedPackage);
+ if (usageStats != null &&
+ usageStats.mEndTimeStamp < removedPackagesMap.valueAt(i)) {
+ stats.packageStats.remove(removedPackage);
+ }
+ }
+
+ // filter out events
+ for (int i = stats.events.size() - 1; i >= 0; i--) {
+ final UsageEvents.Event event = stats.events.get(i);
+ final Long timeRemoved = removedPackagesMap.get(event.mPackage);
+ if (timeRemoved != null && timeRemoved > event.mTimeStamp) {
+ stats.events.remove(i);
+ }
}
}
}
@@ -1226,12 +1231,14 @@
}
void obfuscateCurrentStats(IntervalStats[] currentStats) {
- if (mCurrentVersion < 5) {
- return;
- }
- for (int i = 0; i < currentStats.length; i++) {
- final IntervalStats stats = currentStats[i];
- stats.obfuscateData(mPackagesTokenData);
+ synchronized (mLock) {
+ if (mCurrentVersion < 5) {
+ return;
+ }
+ for (int i = 0; i < currentStats.length; i++) {
+ final IntervalStats stats = currentStats[i];
+ stats.obfuscateData(mPackagesTokenData);
+ }
}
}
diff --git a/telephony/java/android/telephony/PhoneNumberUtils.java b/telephony/java/android/telephony/PhoneNumberUtils.java
index 0ecafc7..019fb7b 100644
--- a/telephony/java/android/telephony/PhoneNumberUtils.java
+++ b/telephony/java/android/telephony/PhoneNumberUtils.java
@@ -49,6 +49,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -1285,6 +1286,13 @@
private static final String SINGAPORE_ISO_COUNTRY_CODE = "SG";
+ private static final String[] COUNTRY_CODES_TO_FORMAT_NATIONALLY = new String[] {
+ "KR", // Korea
+ "JP", // Japan
+ "SG", // Singapore
+ "TW", // Taiwan
+ };
+
/**
* Breaks the given number down and formats it according to the rules
* for the country the number is from.
@@ -1647,45 +1655,58 @@
defaultCountryIso = defaultCountryIso.toUpperCase(Locale.ROOT);
}
+ Rlog.v(LOG_TAG, "formatNumber: defaultCountryIso: " + defaultCountryIso);
+
PhoneNumberUtil util = PhoneNumberUtil.getInstance();
String result = null;
try {
PhoneNumber pn = util.parseAndKeepRawInput(phoneNumber, defaultCountryIso);
- if (KOREA_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
- (pn.getCountryCode() == util.getCountryCodeForRegion(KOREA_ISO_COUNTRY_CODE)) &&
- (pn.getCountryCodeSource() ==
- PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
- /**
- * Need to reformat any local Korean phone numbers (when the user is in Korea) with
- * country code to corresponding national format which would replace the leading
- * +82 with 0.
- */
- result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
- } else if (JAPAN_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
- pn.getCountryCode() == util.getCountryCodeForRegion(JAPAN_ISO_COUNTRY_CODE) &&
- (pn.getCountryCodeSource() ==
- PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
- /**
- * Need to reformat Japanese phone numbers (when user is in Japan) with the national
- * dialing format.
- */
- result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
- } else if (Flags.removeCountryCodeFromLocalSingaporeCalls() &&
- (SINGAPORE_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) &&
- pn.getCountryCode() ==
- util.getCountryCodeForRegion(SINGAPORE_ISO_COUNTRY_CODE) &&
- (pn.getCountryCodeSource() ==
- PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN))) {
- /*
- * Need to reformat Singaporean phone numbers (when the user is in Singapore)
- * with the country code (+65) removed to comply with Singaporean regulations.
- */
- result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+ if (Flags.nationalCountryCodeFormattingForLocalCalls()) {
+ if (Arrays.asList(COUNTRY_CODES_TO_FORMAT_NATIONALLY).contains(defaultCountryIso)
+ && pn.getCountryCode() == util.getCountryCodeForRegion(defaultCountryIso)
+ && pn.getCountryCodeSource()
+ == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN) {
+ return util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+ } else {
+ return util.formatInOriginalFormat(pn, defaultCountryIso);
+ }
} else {
- result = util.formatInOriginalFormat(pn, defaultCountryIso);
+ if (KOREA_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso) && (
+ pn.getCountryCode() == util.getCountryCodeForRegion(KOREA_ISO_COUNTRY_CODE))
+ && (pn.getCountryCodeSource()
+ == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
+ /**
+ * Need to reformat any local Korean phone numbers (when the user is in
+ * Korea) with country code to corresponding national format which would
+ * replace the leading +82 with 0.
+ */
+ result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+ } else if (JAPAN_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso)
+ && pn.getCountryCode() == util.getCountryCodeForRegion(
+ JAPAN_ISO_COUNTRY_CODE) && (pn.getCountryCodeSource()
+ == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN)) {
+ /**
+ * Need to reformat Japanese phone numbers (when user is in Japan) with the
+ * national dialing format.
+ */
+ result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+ } else if (Flags.removeCountryCodeFromLocalSingaporeCalls() && (
+ SINGAPORE_ISO_COUNTRY_CODE.equalsIgnoreCase(defaultCountryIso)
+ && pn.getCountryCode() == util.getCountryCodeForRegion(
+ SINGAPORE_ISO_COUNTRY_CODE) && (pn.getCountryCodeSource()
+ == PhoneNumber.CountryCodeSource.FROM_NUMBER_WITH_PLUS_SIGN))) {
+ /*
+ * Need to reformat Singaporean phone numbers (when the user is in Singapore)
+ * with the country code (+65) removed to comply with Singaporean regulations.
+ */
+ result = util.format(pn, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
+ } else {
+ result = util.formatInOriginalFormat(pn, defaultCountryIso);
+ }
}
} catch (NumberParseException e) {
+ if (DBG) log("formatNumber: NumberParseException caught " + e);
}
return result;
}
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index e29d321..5976657 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -51,10 +51,8 @@
java_library {
name: "android.test.mock.ravenwood",
+ defaults: ["ravenwood-internal-only-visibility-java"],
srcs: [":android-test-mock-sources"],
- visibility: [
- "//frameworks/base",
- ],
}
android_ravenwood_test {
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
index ba0e3db..3c93c88 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
@@ -24,6 +24,7 @@
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
/**
* Data label representation with data shared and data collected maps containing zero or more {@link
@@ -138,7 +139,7 @@
"|",
dataType.getPurposes().stream()
.map(DataType.Purpose::toString)
- .toList()));
+ .collect(Collectors.toList())));
dataLabelsEle.appendChild(hrDataTypeEle);
}
}
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
index d2326d1..284a4b8 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Data usage type representation. Types are specific to a {@link DataCategory} and contains
@@ -182,7 +183,7 @@
XmlUtils.OD_NAME_PURPOSES,
this.getPurposes().stream()
.map(p -> String.valueOf(p.getValue()))
- .toList()));
+ .collect(Collectors.toList())));
}
maybeAddBoolToOdElement(
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
index 26b5639..2c1517b 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
@@ -25,6 +25,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
public class XmlUtils {
public static final String DATA_TYPE_SEPARATOR = "_data_type_";
@@ -131,7 +132,9 @@
/** Gets the top-level children with the tag name.. */
public static List<Element> getChildrenByTagName(Node parentEle, String tagName) {
var elements = XmlUtils.asElementList(parentEle.getChildNodes());
- return elements.stream().filter(e -> e.getTagName().equals(tagName)).toList();
+ return elements.stream()
+ .filter(e -> e.getTagName().equals(tagName))
+ .collect(Collectors.toList());
}
/**
@@ -286,7 +289,8 @@
/** Gets a pipeline-split attribute. */
public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required)
throws MalformedXmlException {
- List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).toList();
+ List<String> list =
+ Arrays.stream(ele.getAttribute(attrName).split("\\|")).collect(Collectors.toList());
if ((list.isEmpty() || list.get(0).isEmpty()) && required) {
throw new MalformedXmlException(
String.format(
@@ -315,7 +319,7 @@
List<Element> boolEles =
XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_BOOLEAN).stream()
.filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
- .toList();
+ .collect(Collectors.toList());
if (boolEles.size() > 1) {
throw new MalformedXmlException(
String.format(
@@ -346,7 +350,7 @@
List<Element> longEles =
XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_LONG).stream()
.filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
- .toList();
+ .collect(Collectors.toList());
if (longEles.size() > 1) {
throw new MalformedXmlException(
String.format(
@@ -377,7 +381,7 @@
List<Element> eles =
XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING).stream()
.filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
- .toList();
+ .collect(Collectors.toList());
if (eles.size() > 1) {
throw new MalformedXmlException(
String.format(
@@ -405,7 +409,7 @@
List<Element> eles =
XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_PBUNDLE_AS_MAP).stream()
.filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
- .toList();
+ .collect(Collectors.toList());
if (eles.size() > 1) {
throw new MalformedXmlException(
String.format(
@@ -449,7 +453,7 @@
List<Element> intArrayEles =
XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_INT_ARRAY).stream()
.filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
- .toList();
+ .collect(Collectors.toList());
if (intArrayEles.size() > 1) {
throw new MalformedXmlException(
String.format("Found more than one %s in %s.", nameName, ele.getTagName()));
@@ -502,7 +506,7 @@
List<Element> arrayEles =
XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING_ARRAY).stream()
.filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
- .toList();
+ .collect(Collectors.toList());
if (arrayEles.size() > 1) {
throw new MalformedXmlException(
String.format(