Merge "Clean up bundleClientTransactionFlag (1/n)" 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/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/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/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/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 7a1ab7b..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.
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/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/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/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/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/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/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/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 438aa768..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/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
+
[email protected]
[email protected]
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/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/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 598db2a9..33d39f0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -44,6 +44,7 @@
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;
@@ -59,7 +60,7 @@
private static final String TAG = "ZenMode";
- public static final String MANUAL_DND_MODE_ID = "manual_dnd";
+ 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 =
@@ -78,24 +79,57 @@
.allowPriorityChannels(false)
.build();
- private final String mId;
- private AutomaticZenRule mRule;
- private final boolean mIsActive;
- private final boolean mIsManualDnd;
-
- public ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
- this(id, rule, isActive, false);
+ public enum Status {
+ ENABLED,
+ ENABLED_AND_ACTIVE,
+ DISABLED_BY_USER,
+ DISABLED_BY_OTHER
}
- private ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd) {
- mId = id;
- mRule = rule;
- mIsActive = isActive;
- mIsManualDnd = isManualDnd;
+ 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, true);
+ 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
@@ -109,6 +143,26 @@
}
@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) {
@@ -225,33 +279,29 @@
}
public boolean isActive() {
- return mIsActive;
+ return mStatus == Status.ENABLED_AND_ACTIVE;
}
public boolean isSystemOwned() {
return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
}
- @AutomaticZenRule.Type
- public int getType() {
- return mRule.getType();
- }
-
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof ZenMode other
&& mId.equals(other.mId)
&& mRule.equals(other.mRule)
- && mIsActive == other.mIsActive;
+ && mStatus.equals(other.mStatus)
+ && mIsManualDnd == other.mIsManualDnd;
}
@Override
public int hashCode() {
- return Objects.hash(mId, mRule, mIsActive);
+ return Objects.hash(mId, mRule, mStatus, mIsManualDnd);
}
@Override
public String toString() {
- return mId + "(" + (mIsActive ? "active" : "inactive") + ") -> " + mRule;
+ 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
index d07d743..5529da0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
@@ -31,6 +31,7 @@
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -64,25 +65,27 @@
}
public List<ZenMode> getModes() {
- ArrayList<ZenMode> modes = new ArrayList<>();
+ Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+
+ ArrayList<ZenMode> modes = new ArrayList<>();
modes.add(getManualDndMode(currentConfig));
- Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
String ruleId = zenRuleEntry.getKey();
- modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(),
- isRuleActive(ruleId, currentConfig)));
+ 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");
+ }
}
- modes.sort((l, r) -> {
- if (l.isManualDnd()) {
- return -1;
- } else if (r.isManualDnd()) {
- return 1;
- }
- return l.getRule().getName().compareTo(r.getRule().getName());
- });
+ // Manual DND first, then alphabetically.
+ modes.sort(Comparator.comparing(ZenMode::isManualDnd).reversed()
+ .thenComparing(ZenMode::getName));
return modes;
}
@@ -94,10 +97,11 @@
return getManualDndMode(currentConfig);
} else {
AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
- if (rule == null) {
+ ZenModeConfig.ZenRule extraData = currentConfig.automaticRules.get(id);
+ if (rule == null || extraData == null) {
return null;
}
- return new ZenMode(id, rule, isRuleActive(id, currentConfig));
+ return new ZenMode(id, rule, extraData);
}
}
@@ -117,15 +121,6 @@
return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
}
- private static boolean isRuleActive(String id, ZenModeConfig config) {
- if (config == null) {
- // shouldn't happen if the config is coming from NM, but be safe
- return false;
- }
- ZenModeConfig.ZenRule configRule = config.automaticRules.get(id);
- return configRule != null && configRule.isAutomaticActive();
- }
-
public void updateMode(ZenMode mode) {
if (mode.isManualDnd()) {
try {
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
index 43aba45..32cdb98 100644
--- 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
@@ -24,6 +24,8 @@
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;
@@ -37,6 +39,7 @@
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)
@@ -44,7 +47,7 @@
@Test
public void testBasicMethods() {
- ZenMode zenMode = new ZenMode("id", ZEN_RULE, true);
+ ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true));
assertThat(zenMode.getId()).isEqualTo("id");
assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
@@ -60,21 +63,63 @@
}
@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() {
- ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(ZEN_POLICY)
- .build(), false);
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
}
@Test
public void getPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
- ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
.setZenPolicy(ZEN_POLICY) // should be ignored
- .build(), false);
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
assertThat(zenMode.getPolicy()).isEqualTo(
new ZenPolicy.Builder()
@@ -87,10 +132,11 @@
@Test
public void getPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
- ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setInterruptionFilter(INTERRUPTION_FILTER_NONE)
.setZenPolicy(ZEN_POLICY) // should be ignored
- .build(), false);
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
assertThat(zenMode.getPolicy()).isEqualTo(
new ZenPolicy.Builder()
@@ -102,9 +148,10 @@
@Test
public void setPolicy_setsInterruptionFilterPriority() {
- ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
.setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
- .build(), false);
+ .build();
+ ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false));
zenMode.setPolicy(ZEN_POLICY);
@@ -113,4 +160,15 @@
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
index 7c7972d..00c7ae3 100644
--- 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
@@ -22,6 +22,7 @@
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;
@@ -42,7 +43,6 @@
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.service.notification.Condition;
-import android.service.notification.ZenAdapters;
import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;
@@ -93,7 +93,7 @@
SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
// Helper methods to add active/inactive rule state to a config. Returns a copy.
- private ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
+ private static ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
ZenModeConfig out = base.copy();
if (active) {
@@ -108,22 +108,31 @@
return out;
}
- private ZenModeConfig configWithRule(ZenModeConfig base, String ruleId, AutomaticZenRule rule,
- boolean active) {
+ 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 = active;
+ zenRule.enabled = azr.isEnabled();
zenRule.snoozing = false;
- zenRule.condition = new Condition(rule.getConditionId(), "",
+ zenRule.conditionId = azr.getConditionId();
+ zenRule.condition = new Condition(azr.getConditionId(), "",
active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
Condition.SOURCE_USER_ACTION);
- out.automaticRules.put(ruleId, zenRule);
+ return zenRule;
+ }
- return out;
+ private static ZenMode newZenMode(String id, AutomaticZenRule azr, boolean active) {
+ return new ZenMode(id, azr, zenConfigRuleForRule(id, azr, active));
}
@Before
@@ -151,15 +160,17 @@
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
- ZenPolicy zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(dndPolicy);
assertThat(modes).containsExactly(
ZenMode.manualDndMode(
new AutomaticZenRule.Builder(
@@ -167,12 +178,12 @@
Uri.EMPTY)
.setType(AutomaticZenRule.TYPE_OTHER)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
- .setZenPolicy(zenPolicy)
+ .setZenPolicy(notificationPolicyToZenPolicy(dndPolicy))
.setManualInvocationAllowed(true)
.build(),
false),
- new ZenMode("rule2", rule2, false),
- new ZenMode("rule1", ZEN_RULE, false))
+ newZenMode("rule2", rule2, false),
+ newZenMode("rule1", ZEN_RULE, false))
.inOrder();
}
@@ -192,7 +203,7 @@
mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
.setType(AutomaticZenRule.TYPE_OTHER)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
- .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(dndPolicy))
+ .setZenPolicy(notificationPolicyToZenPolicy(dndPolicy))
.setManualInvocationAllowed(true)
.build(), false));
}
@@ -200,10 +211,12 @@
@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(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+ assertThat(mode).isEqualTo(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
}
@Test
@@ -230,11 +243,13 @@
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();
}
@@ -252,11 +267,13 @@
// 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();
}
@@ -292,7 +309,7 @@
@Test
public void updateMode_zenRule_updatesRule() {
- ZenMode ruleMode = new ZenMode("rule", ZEN_RULE, false);
+ ZenMode ruleMode = newZenMode("rule", ZEN_RULE, false);
mBackend.updateMode(ruleMode);
@@ -320,7 +337,7 @@
@Test
public void activateMode_zenRule_setsRuleStateActive() {
- mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false), null);
+ 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,
@@ -330,7 +347,7 @@
@Test
public void activateMode_zenRuleWithDuration_fails() {
assertThrows(IllegalArgumentException.class,
- () -> mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false),
+ () -> mBackend.activateMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false),
Duration.ofMinutes(30)));
}
@@ -343,7 +360,7 @@
@Test
public void deactivateMode_zenRule_setsRuleStateInactive() {
- mBackend.deactivateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+ 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,
@@ -352,7 +369,7 @@
@Test
public void removeMode_zenRule_deletesRule() {
- mBackend.removeMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+ mBackend.removeMode(newZenMode(ZEN_RULE_ID, ZEN_RULE, false));
verify(mNm).removeAutomaticZenRule(ZEN_RULE_ID, true);
}
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/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/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/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/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/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/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/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/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index f3692bd..2f40c99 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
},
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..9cc0b3c 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
@@ -25,6 +25,7 @@
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.launch
@@ -48,6 +49,37 @@
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
+ // 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,24 +100,6 @@
}
}
- /**
- * 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)
}
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/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/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/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/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/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/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/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/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/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/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/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 5e08b0bc..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
@@ -66,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
@@ -90,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,
@@ -137,6 +140,8 @@
remoteViewCache,
entry,
conversationProcessor,
+ ronExtractor,
+ ronInflater,
row,
bindParams.isMinimized,
bindParams.usesIncreasedHeight,
@@ -181,6 +186,7 @@
notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
headsUpStyleProvider = headsUpStyleProvider,
conversationProcessor = conversationProcessor,
+ ronExtractor = ronExtractor,
logger = logger,
)
inflateSmartReplyViews(
@@ -260,6 +266,8 @@
when (inflateFlag) {
FLAG_CONTENT_VIEW_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)
}
@@ -337,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,
@@ -416,6 +426,7 @@
notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
headsUpStyleProvider = headsUpStyleProvider,
conversationProcessor = conversationProcessor,
+ ronExtractor = ronExtractor,
logger = logger
)
logger.logAsyncTaskProgress(
@@ -447,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
@@ -552,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
@@ -586,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)
@@ -627,6 +655,7 @@
notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
headsUpStyleProvider: HeadsUpStyleProvider,
conversationProcessor: ConversationNotificationProcessor,
+ ronExtractor: RichOngoingNotificationContentExtractor,
logger: NotificationRowContentBinderLogger
): InflationProgress {
// process conversations and extract the messaging style
@@ -635,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,
@@ -672,6 +716,7 @@
NotificationContentModel(
headsUpStatusBarModel = headsUpStatusBarModel,
singleLineViewModel = singleLineViewModel,
+ richOngoingContentModel = richOngoingContentModel,
)
return InflationProgress(
@@ -799,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,
@@ -813,7 +858,7 @@
}
override val remoteView: RemoteViews
- get() = result.remoteViews.contracted!!
+ get() = result.remoteViews.contracted
}
logger.logAsyncTaskProgress(entry, "applying contracted view")
applyRemoteView(
@@ -838,88 +883,82 @@
)
}
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(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(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) {
@@ -1332,16 +1371,33 @@
return false
}
logger.logAsyncTaskProgress(entry, "finishing")
- setViewsFromRemoteViews(
- reInflateFlags,
+
+ // 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
+ }
+
+ // 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,
)
- result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
+ // set single line view
if (
AsyncHybridViewInflation.isEnabled &&
reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
@@ -1357,14 +1413,29 @@
row.privateLayout.setSingleLineView(result.inflatedSingleLineView)
}
}
- entry.setContentModel(result.contentModel)
+
+ // 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)
+ }
+
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
endListener?.onAsyncInflationFinished(entry)
return true
}
- private fun setViewsFromRemoteViews(
- reInflateFlags: Int,
+ private fun setContentViewsFromRemoteViews(
+ @InflationFlag reInflateFlags: Int,
entry: NotificationEntry,
remoteViewCache: NotifRemoteViewCache,
result: InflationProgress,
@@ -1521,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/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/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/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/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/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/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/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/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/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/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
new file mode 100644
index 0000000..3a7d7ba
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+
+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/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/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/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..45ab62d 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);
+ }
}
}
@@ -19907,7 +19909,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 +19917,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/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/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/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 @@
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
per-file ThermalManagerService.java=file:/THERMAL_OWNERS
per-file [email protected]
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/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 0a384e5..21155bb 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -11154,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/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/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/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/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/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(