Merge "Fix rule ownership check when notifying from a ConditionProviderService" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 69892f9b..b1c091b 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -107,6 +107,7 @@
"com.android.server.flags.services-aconfig-java",
"com.android.text.flags-aconfig-java",
"com.android.window.flags.window-aconfig-java",
+ "configinfra_framework_flags_java_lib",
"conscrypt_exported_aconfig_flags_lib",
"device_policy_aconfig_flags_lib",
"display_flags_lib",
@@ -1584,6 +1585,13 @@
}
java_aconfig_library {
+ name: "android.app.appfunctions.flags-aconfig-java-host",
+ aconfig_declarations: "android.app.appfunctions.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ host_supported: true,
+}
+
+java_aconfig_library {
name: "android.app.appfunctions.exported-flags-aconfig-java",
aconfig_declarations: "android.app.appfunctions.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/Android.bp b/Android.bp
index a1f6e30..9d8c8a6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -408,7 +408,6 @@
"bouncycastle-repackaged-unbundled",
"com.android.sysprop.foldlockbehavior",
"com.android.sysprop.view",
- "configinfra_framework_flags_java_lib",
"framework-internal-utils",
"dynamic_instrumentation_manager_aidl-java",
// If MimeMap ever becomes its own APEX, then this dependency would need to be removed
@@ -537,45 +536,6 @@
}),
}
-// This is identical to "framework-minus-apex" but with "jarjar_shards" hardcodd.
-// (also "stem" is commented out to avoid a conflict with the "framework-minus-apex")
-// TODO(b/383559945) This module is just for local testing / verification. It's not used
-// by anything. Remove it once we roll out RELEASE_USE_SHARDED_JARJAR_ON_FRAMEWORK_MINUS_APEX.
-java_library {
- name: "framework-minus-apex_jarjar-sharded",
- defaults: [
- "framework-minus-apex-with-libs-defaults",
- "framework-non-updatable-lint-defaults",
- ],
- installable: true,
- // For backwards compatibility.
- // stem: "framework",
- apex_available: ["//apex_available:platform"],
- visibility: [
- "//frameworks/base",
- "//frameworks/base/location",
- // TODO(b/147128803) remove the below lines
- "//frameworks/base/apex/blobstore/framework",
- "//frameworks/base/apex/jobscheduler/framework",
- "//frameworks/base/packages/Tethering/tests/unit",
- "//packages/modules/Connectivity/Tethering/tests/unit",
- ],
- errorprone: {
- javacflags: [
- "-Xep:AndroidFrameworkCompatChange:ERROR",
- "-Xep:AndroidFrameworkUid:ERROR",
- ],
- },
- lint: {
- baseline_filename: "lint-baseline.xml",
- warning_checks: [
- "FlaggedApi",
- ],
- },
- jarjar_prefix: "com.android.internal.hidden_from_bootclasspath",
- jarjar_shards: "10",
-}
-
java_library {
name: "framework-minus-apex-intdefs",
defaults: ["framework-minus-apex-with-libs-defaults"],
diff --git a/core/api/current.txt b/core/api/current.txt
index 59eb31a..c410939 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8947,18 +8947,19 @@
method public android.content.ClipData getClipData();
method public android.os.Bundle getExtras();
method public android.content.Intent getIntent();
+ method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") @Nullable public android.net.Uri getSessionTransferUri();
method public String getStructuredData();
method public android.net.Uri getWebUri();
method public boolean isAppProvidedIntent();
method public boolean isAppProvidedWebUri();
method public void setClipData(android.content.ClipData);
method public void setIntent(android.content.Intent);
+ method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public void setSessionTransferUri(@Nullable android.net.Uri);
method public void setStructuredData(String);
method public void setWebUri(android.net.Uri);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.assist.AssistContent> CREATOR;
field @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public static final String EXTRA_APP_FUNCTION_DATA = "android.app.assist.extra.APP_FUNCTION_DATA";
- field @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public static final String EXTRA_SESSION_TRANSFER_WEB_URI = "android.app.assist.extra.SESSION_TRANSFER_WEB_URI";
}
public class AssistStructure implements android.os.Parcelable {
@@ -9190,8 +9191,9 @@
package android.app.jank {
@FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats {
- ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram);
+ ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram);
method public long getJankyFrameCount();
+ method @Nullable public String getNavigationComponent();
method @NonNull public android.app.jank.RelativeFrameTimeHistogram getRelativeFrameTimeHistogram();
method public long getTotalFrameCount();
method public int getUid();
@@ -33657,16 +33659,23 @@
}
@FlaggedApi("android.os.cpu_gpu_headrooms") public final class CpuHeadroomParams {
- ctor public CpuHeadroomParams();
method public int getCalculationType();
- method @IntRange(from=0x32, to=0x2710) public long getCalculationWindowMillis();
- method public void setCalculationType(int);
- method public void setCalculationWindowMillis(@IntRange(from=0x32, to=0x2710) int);
- method public void setTids(@NonNull int...);
+ method public long getCalculationWindowMillis();
+ method @NonNull public int[] getTids();
+ method @NonNull public android.os.CpuHeadroomParams.Builder toBuilder();
field public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
field public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
}
+ public static final class CpuHeadroomParams.Builder {
+ ctor public CpuHeadroomParams.Builder();
+ ctor public CpuHeadroomParams.Builder(@NonNull android.os.CpuHeadroomParams);
+ method @NonNull public android.os.CpuHeadroomParams build();
+ method @NonNull public android.os.CpuHeadroomParams.Builder setCalculationType(int);
+ method @NonNull public android.os.CpuHeadroomParams.Builder setCalculationWindowMillis(@IntRange(from=1) int);
+ method @NonNull public android.os.CpuHeadroomParams.Builder setTids(@NonNull int...);
+ }
+
public final class CpuUsageInfo implements android.os.Parcelable {
method public int describeContents();
method public long getActive();
@@ -33915,13 +33924,20 @@
}
@FlaggedApi("android.os.cpu_gpu_headrooms") public final class GpuHeadroomParams {
- ctor public GpuHeadroomParams();
method public int getCalculationType();
- method @IntRange(from=0x32, to=0x2710) public int getCalculationWindowMillis();
- method public void setCalculationType(int);
- method public void setCalculationWindowMillis(@IntRange(from=0x32, to=0x2710) int);
+ method public int getCalculationWindowMillis();
field public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1
field public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0
+ field public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000; // 0x2710
+ field public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50; // 0x32
+ }
+
+ public static final class GpuHeadroomParams.Builder {
+ ctor public GpuHeadroomParams.Builder();
+ ctor public GpuHeadroomParams.Builder(@NonNull android.os.GpuHeadroomParams);
+ method @NonNull public android.os.GpuHeadroomParams build();
+ method @NonNull public android.os.GpuHeadroomParams.Builder setCalculationType(int);
+ method @NonNull public android.os.GpuHeadroomParams.Builder setCalculationWindowMillis(@IntRange(from=1) int);
}
public class Handler {
@@ -35187,9 +35203,12 @@
public class SystemHealthManager {
method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getCpuHeadroom(@Nullable android.os.CpuHeadroomParams);
+ method @FlaggedApi("android.os.cpu_gpu_headrooms") @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getCpuHeadroomCalculationWindowRange();
method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getCpuHeadroomMinIntervalMillis();
method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getGpuHeadroom(@Nullable android.os.GpuHeadroomParams);
+ method @FlaggedApi("android.os.cpu_gpu_headrooms") @NonNull public android.util.Pair<java.lang.Integer,java.lang.Integer> getGpuHeadroomCalculationWindowRange();
method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getGpuHeadroomMinIntervalMillis();
+ method @FlaggedApi("android.os.cpu_gpu_headrooms") @IntRange(from=1) public int getMaxCpuHeadroomTidsSize();
method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>);
method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>);
method public android.os.health.HealthStats takeMyUidSnapshot();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ed8042d..cd2cc07 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2,7 +2,7 @@
package android {
public static final class Manifest.permission {
- field @FlaggedApi("com.android.server.accessibility.motion_event_observing") public static final String ACCESSIBILITY_MOTION_EVENT_OBSERVING = "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING";
+ field public static final String ACCESSIBILITY_MOTION_EVENT_OBSERVING = "android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING";
field public static final String ACCESS_NOTIFICATIONS = "android.permission.ACCESS_NOTIFICATIONS";
field public static final String ACTIVITY_EMBEDDING = "android.permission.ACTIVITY_EMBEDDING";
field public static final String ADJUST_RUNTIME_PERMISSIONS_POLICY = "android.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY";
@@ -2113,6 +2113,11 @@
method public boolean isAidlHal();
}
+ public static final class MediaMuxer.OutputFormat {
+ field public static final int MUXER_OUTPUT_FIRST = 0; // 0x0
+ field public static final int MUXER_OUTPUT_LAST = 4; // 0x4
+ }
+
public final class MediaRoute2Info implements android.os.Parcelable {
method @NonNull public String getOriginalId();
}
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 349b4ed..fe23517 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -1977,6 +1977,8 @@
Documentation mentions 'TODO'
+UnflaggedApi: android.Manifest.permission#ACCESSIBILITY_MOTION_EVENT_OBSERVING:
+ New API must be flagged with @FlaggedApi: field android.Manifest.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING
UnflaggedApi: android.Manifest.permission#MANAGE_REMOTE_AUTH:
New API must be flagged with @FlaggedApi: field android.Manifest.permission.MANAGE_REMOTE_AUTH
UnflaggedApi: android.Manifest.permission#RESERVED_FOR_TESTING_SIGNATURE:
@@ -2057,6 +2059,10 @@
New API must be flagged with @FlaggedApi: method android.media.AudioManager.getFocusFadeOutDurationForTest()
UnflaggedApi: android.media.AudioManager#getFocusUnmuteDelayAfterFadeOutForTest():
New API must be flagged with @FlaggedApi: method android.media.AudioManager.getFocusUnmuteDelayAfterFadeOutForTest()
+UnflaggedApi: android.media.MediaMuxer.OutputFormat#MUXER_OUTPUT_FIRST:
+ New API must be flagged with @FlaggedApi: field android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_FIRST
+UnflaggedApi: android.media.MediaMuxer.OutputFormat#MUXER_OUTPUT_LAST:
+ New API must be flagged with @FlaggedApi: field android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_LAST
UnflaggedApi: android.media.RingtoneSelection:
New API must be flagged with @FlaggedApi: class android.media.RingtoneSelection
UnflaggedApi: android.media.RingtoneSelection#DEFAULT_SELECTION_URI_STRING:
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 8614bde..b198811 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1273,8 +1273,8 @@
* Requests to show the “Open in browser” education. “Open in browser” is a feature
* within the app header that allows users to switch from an app to the web. The feature
* is made available when an application is opened by a user clicking a link or when a
- * link is provided by an application. Links can be provided by utilizing
- * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+ * link is provided by an application. Links can be provided by calling
+ * {@link AssistContent#setSessionTransferUri} or
* {@link AssistContent#setWebUri}.
*
* <p>This method should be utilized when an activity wants to nudge the user to switch
@@ -1287,7 +1287,7 @@
* disruptive to the user to show the education and when it is optimal to switch the user to a
* browser session. Before requesting to show the education, developers should assert that they
* have set a link that can be used by the "Open in browser" feature through either
- * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or
+ * {@link AssistContent#setSessionTransferUri} or
* {@link AssistContent#setWebUri} so that users are navigated to a relevant page if they choose
* to switch to the browser. If a URI is not set using either method, "Open in browser" will
* utilize a generic link if available which will direct users to the homepage of the site
@@ -1296,7 +1296,7 @@
* the user will not be provided with the option to switch to the browser and the education will
* not be shown if requested.
*
- * @see android.app.assist.AssistContent#EXTRA_SESSION_TRANSFER_WEB_URI
+ * @see android.app.assist.AssistContent#setSessionTransferUri
*/
@FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
public final void requestOpenInBrowserEducation() {
diff --git a/core/java/android/app/IUserSwitchObserver.aidl b/core/java/android/app/IUserSwitchObserver.aidl
index 1ff7a17..d71ee7c 100644
--- a/core/java/android/app/IUserSwitchObserver.aidl
+++ b/core/java/android/app/IUserSwitchObserver.aidl
@@ -19,10 +19,10 @@
import android.os.IRemoteCallback;
/** {@hide} */
-interface IUserSwitchObserver {
- void onBeforeUserSwitching(int newUserId);
- oneway void onUserSwitching(int newUserId, IRemoteCallback reply);
- oneway void onUserSwitchComplete(int newUserId);
- oneway void onForegroundProfileSwitch(int newProfileId);
- oneway void onLockedBootComplete(int newUserId);
+oneway interface IUserSwitchObserver {
+ void onBeforeUserSwitching(int newUserId, IRemoteCallback reply);
+ void onUserSwitching(int newUserId, IRemoteCallback reply);
+ void onUserSwitchComplete(int newUserId);
+ void onForegroundProfileSwitch(int newProfileId);
+ void onLockedBootComplete(int newUserId);
}
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 5567c08..e7e9b00 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -36,6 +36,7 @@
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import android.util.SystemPropertySetter;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -157,21 +158,6 @@
public static final String MODULE_TELEPHONY = "telephony";
/**
- * Constants that affect retries when the process is unable to write the property.
- * The first constant is the number of times the process will attempt to set the
- * property. The second constant is the delay between attempts.
- */
-
- /**
- * Wait 200ms between retry attempts and the retry limit is 5. That gives a total possible
- * delay of 1s, which should be less than ANR timeouts. The goal is to have the system crash
- * because the property could not be set (which is a condition that is easily recognized) and
- * not crash because of an ANR (which can be confusing to debug).
- */
- private static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
- private static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
-
- /**
* Construct a system property that matches the rules described above. The module is
* one of the permitted values above. The API is a string that is a legal Java simple
* identifier. The api is modified to conform to the system property style guide by
@@ -958,37 +944,8 @@
*/
@Override
void setNonceInternal(long value) {
- // Failing to set the nonce is a fatal error. Failures setting a system property have
- // been reported; given that the failure is probably transient, this function includes
- // a retry.
final String str = Long.toString(value);
- RuntimeException failure = null;
- for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
- try {
- SystemProperties.set(mName, str);
- if (attempt > 0) {
- // This log is not guarded. Based on known bug reports, it should
- // occur once a week or less. The purpose of the log message is to
- // identify the retries as a source of delay that might be otherwise
- // be attributed to the cache itself.
- Log.w(TAG, "Nonce set after " + attempt + " tries");
- }
- return;
- } catch (RuntimeException e) {
- if (failure == null) {
- failure = e;
- }
- try {
- Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
- } catch (InterruptedException x) {
- // Ignore this exception. The desired delay is only approximate and
- // there is no issue if the sleep sometimes terminates early.
- }
- }
- }
- // This point is reached only if SystemProperties.set() fails at least once.
- // Rethrow the first exception that was received.
- throw failure;
+ SystemPropertySetter.setWithRetry(mName, str);
}
}
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 2e6f3e1..5754984 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -753,7 +753,7 @@
* <p>
* The mode can be one of:
* <ul>
- * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+ * <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into
* {@code notnight} mode</li>
* <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
* {@code night} mode</li>
@@ -889,7 +889,7 @@
* <p>
* The mode can be one of:
* <ul>
- * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into
+ * <li><em>{@link #MODE_NIGHT_NO}</em> sets the device into
* {@code notnight} mode</li>
* <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into
* {@code night} mode</li>
diff --git a/core/java/android/app/UserSwitchObserver.java b/core/java/android/app/UserSwitchObserver.java
index 727799a1..1664cfb 100644
--- a/core/java/android/app/UserSwitchObserver.java
+++ b/core/java/android/app/UserSwitchObserver.java
@@ -30,7 +30,11 @@
}
@Override
- public void onBeforeUserSwitching(int newUserId) throws RemoteException {}
+ public void onBeforeUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
+ if (reply != null) {
+ reply.sendResult(null);
+ }
+ }
@Override
public void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException {
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index af035cb..75589fa 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -160,16 +160,6 @@
}
flag {
- name: "fix_race_condition_in_tie_profile_lock"
- namespace: "enterprise"
- description: "Fix race condition in tieProfileLockIfNecessary()"
- bug: "355905501"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "quiet_mode_credential_bug_fix"
namespace: "enterprise"
description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped."
diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java
index 3e3ca24..adf8c94 100644
--- a/core/java/android/app/assist/AssistContent.java
+++ b/core/java/android/app/assist/AssistContent.java
@@ -1,6 +1,7 @@
package android.app.assist;
import android.annotation.FlaggedApi;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ClipData;
import android.content.Intent;
@@ -30,31 +31,6 @@
public static final String EXTRA_APP_FUNCTION_DATA =
"android.app.assist.extra.APP_FUNCTION_DATA";
- /**
- * This extra can be optionally supplied in the {@link #getExtras} bundle to provide a
- * {@link Uri} which will be utilized when transitioning a user's session to another surface.
- *
- * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the
- * "Open in browser" feature will use this URI to transition the current session from one
- * surface to the other. Apps may choose to encode session or user information into this
- * URI in order to provide a better session transfer experience.
- *
- * <p>Unlike {@link #setWebUri}, this URI will not be used for features where the user might
- * accidentally share it with another user. However, developers should not encode
- * authentication credentials into this URI, because it will be surfaced in the browser URL
- * bar and may be copied and shared from there.
- *
- * <p>When providing this extra, developers should still continue to provide
- * {@link #setWebUri} for backwards compatibility with features such as
- * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing">
- * recents URL sharing</a> which do not benefit from a session-transfer web URI.
- *
- * @see android.app.Activity#requestOpenInBrowserEducation()
- */
- @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
- public static final String EXTRA_SESSION_TRANSFER_WEB_URI =
- "android.app.assist.extra.SESSION_TRANSFER_WEB_URI";
-
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private boolean mIsAppProvidedIntent = false;
private boolean mIsAppProvidedWebUri = false;
@@ -66,6 +42,7 @@
private ClipData mClipData;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private Uri mUri;
+ private Uri mSessionTransferUri;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private final Bundle mExtras;
@@ -200,6 +177,41 @@
}
/**
+ * This method can be used to provide a {@link Uri} which will be utilized when transitioning a
+ * user's session to another surface.
+ *
+ * <p>If provided, instead of using the URI provided in {@link #setWebUri}, the
+ * "Open in browser" feature will use this URI to transition the current session from one
+ * surface to the other. Apps may choose to encode session or user information into this
+ * URI in order to provide a better session transfer experience. However, while this URI will
+ * only be available to the system and not other applications, developers should not encode
+ * authentication credentials into this URI, because it will be surfaced in the browser URL bar
+ * and may be copied and shared from there.
+ *
+ * <p>When providing this URI, developers should still continue to provide
+ * {@link #setWebUri} for backwards compatibility with features such as
+ * <a href="https://developer.android.com/guide/components/activities/recents#url-sharing">
+ * recents URL sharing</a> which facilitate link sharing with other users and would not benefit
+ * from a session-transfer URI.
+ *
+ * @see android.app.Activity#requestOpenInBrowserEducation()
+ */
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
+ public void setSessionTransferUri(@Nullable Uri uri) {
+ mSessionTransferUri = uri;
+ }
+
+ /**
+ * Return the content's session transfer web URI as per
+ * {@link #setSessionTransferUri(android.net.Uri)}, or null if there is none.
+ */
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION)
+ @Nullable
+ public Uri getSessionTransferUri() {
+ return mSessionTransferUri;
+ }
+
+ /**
* Return Bundle for extra vendor-specific data that can be modified and examined.
*/
public Bundle getExtras() {
@@ -218,6 +230,9 @@
mUri = Uri.CREATOR.createFromParcel(in);
}
if (in.readInt() != 0) {
+ mSessionTransferUri = Uri.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() != 0) {
mStructuredData = in.readString();
}
mIsAppProvidedIntent = in.readInt() == 1;
@@ -245,6 +260,12 @@
} else {
dest.writeInt(0);
}
+ if (mSessionTransferUri != null) {
+ dest.writeInt(1);
+ mSessionTransferUri.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
if (mStructuredData != null) {
dest.writeInt(1);
dest.writeString(mStructuredData);
diff --git a/core/java/android/app/jank/AppJankStats.java b/core/java/android/app/jank/AppJankStats.java
index 6ef6a44..a8ebc383 100644
--- a/core/java/android/app/jank/AppJankStats.java
+++ b/core/java/android/app/jank/AppJankStats.java
@@ -57,6 +57,8 @@
// Histogram of relative frame times encoded in predetermined buckets.
private RelativeFrameTimeHistogram mRelativeFrameTimeHistogram;
+ // Navigation component associated to this stat.
+ private String mNavigationComponent;
/** Used to indicate no widget category has been set. */
public static final String WIDGET_CATEGORY_UNSPECIFIED = "unspecified";
@@ -158,6 +160,8 @@
*
* @param appUid the Uid of the App that is collecting jank stats.
* @param widgetId the widget id that frames will be associated to.
+ * @param navigationComponent the intended navigation target within the activity, this could be
+ * a navigation destination, screen and/or pane.
* @param widgetCategory a category used to organize widgets in a structured way that indicates
* they serve a similar purpose or perform related functions. Must be
* prefixed with WIDGET_CATEGORY_ and have a suffix of one of the
@@ -172,14 +176,14 @@
* @param jankyFrames the total number of janky frames that were counted for this stat.
* @param relativeFrameTimeHistogram the histogram with predefined buckets. See
* {@link #getRelativeFrameTimeHistogram()} for details.
- *
*/
- public AppJankStats(int appUid, @NonNull String widgetId,
+ public AppJankStats(int appUid, @NonNull String widgetId, @Nullable String navigationComponent,
@Nullable @WidgetCategory String widgetCategory,
@Nullable @WidgetState String widgetState, long totalFrames, long jankyFrames,
@NonNull RelativeFrameTimeHistogram relativeFrameTimeHistogram) {
mUid = appUid;
mWidgetId = widgetId;
+ mNavigationComponent = navigationComponent;
mWidgetCategory = widgetCategory != null ? widgetCategory : WIDGET_CATEGORY_UNSPECIFIED;
mWidgetState = widgetState != null ? widgetState : WIDGET_STATE_UNSPECIFIED;
mTotalFrames = totalFrames;
@@ -254,4 +258,13 @@
public @NonNull RelativeFrameTimeHistogram getRelativeFrameTimeHistogram() {
return mRelativeFrameTimeHistogram;
}
+
+ /**
+ * Returns the navigation component if it exists that this stat applies to.
+ *
+ * @return the navigation component if it exists that this stat applies to.
+ */
+ public @Nullable String getNavigationComponent() {
+ return mNavigationComponent;
+ }
}
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 46da4a3..f31e7d4 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -11,14 +11,6 @@
container: "system"
flag {
- name: "enable_native_vdm"
- namespace: "virtual_devices"
- description: "Enable native VDM service"
- bug: "303535376"
- is_fixed_read_only: true
-}
-
-flag {
name: "dynamic_policy"
is_exported: true
namespace: "virtual_devices"
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 0333942..9d11710 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -17,6 +17,7 @@
package android.content.pm;
import android.Manifest;
+import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -30,6 +31,7 @@
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
+import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.AttributeSet;
import android.util.IntArray;
@@ -45,11 +47,11 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
-import libcore.io.IoUtils;
-
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
+import libcore.io.IoUtils;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -94,6 +96,9 @@
@GuardedBy("mServicesLock")
private final SparseArray<UserServices<V>> mUserServices = new SparseArray<UserServices<V>>(2);
+ @GuardedBy("mServicesLock")
+ private final ArrayMap<String, ServiceInfo<V>> mServiceInfoCaches = new ArrayMap<>();
+
private static class UserServices<V> {
@GuardedBy("mServicesLock")
final Map<V, Integer> persistentServices = Maps.newHashMap();
@@ -323,13 +328,16 @@
public final ComponentName componentName;
@UnsupportedAppUsage
public final int uid;
+ public final long lastUpdateTime;
/** @hide */
- public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName) {
+ public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName,
+ long lastUpdateTime) {
this.type = type;
this.componentInfo = componentInfo;
this.componentName = componentName;
this.uid = (componentInfo != null) ? componentInfo.applicationInfo.uid : -1;
+ this.lastUpdateTime = lastUpdateTime;
}
@Override
@@ -490,7 +498,7 @@
final List<ResolveInfo> resolveInfos = queryIntentServices(userId);
for (ResolveInfo resolveInfo : resolveInfos) {
try {
- ServiceInfo<V> info = parseServiceInfo(resolveInfo);
+ ServiceInfo<V> info = parseServiceInfo(resolveInfo, userId);
if (info == null) {
Log.w(TAG, "Unable to load service info " + resolveInfo.toString());
continue;
@@ -638,13 +646,31 @@
}
@VisibleForTesting
- protected ServiceInfo<V> parseServiceInfo(ResolveInfo service)
+ protected ServiceInfo<V> parseServiceInfo(ResolveInfo service, int userId)
throws XmlPullParserException, IOException {
android.content.pm.ServiceInfo si = service.serviceInfo;
ComponentName componentName = new ComponentName(si.packageName, si.name);
PackageManager pm = mContext.getPackageManager();
+ // Check if the service has been in the service cache.
+ long lastUpdateTime = -1;
+ if (Flags.optimizeParsingInRegisteredServicesCache()) {
+ try {
+ PackageInfo packageInfo = pm.getPackageInfoAsUser(si.packageName,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
+ lastUpdateTime = packageInfo.lastUpdateTime;
+
+ ServiceInfo<V> serviceInfo = getServiceInfoFromServiceCache(si, lastUpdateTime);
+ if (serviceInfo != null) {
+ return serviceInfo;
+ }
+ } catch (NameNotFoundException | SecurityException e) {
+ Slog.d(TAG, "Fail to get the PackageInfo in parseServiceInfo: " + e);
+ }
+ }
+
XmlResourceParser parser = null;
try {
parser = si.loadXmlMetaData(pm, mMetaDataName);
@@ -670,8 +696,13 @@
if (v == null) {
return null;
}
- final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo;
- return new ServiceInfo<V>(v, serviceInfo, componentName);
+ ServiceInfo<V> serviceInfo = new ServiceInfo<V>(v, si, componentName, lastUpdateTime);
+ if (Flags.optimizeParsingInRegisteredServicesCache()) {
+ synchronized (mServicesLock) {
+ mServiceInfoCaches.put(getServiceCacheKey(si), serviceInfo);
+ }
+ }
+ return serviceInfo;
} catch (NameNotFoundException e) {
throw new XmlPullParserException(
"Unable to load resources for pacakge " + si.packageName);
@@ -841,4 +872,28 @@
mContext.unregisterReceiver(mExternalReceiver);
mContext.unregisterReceiver(mUserRemovedReceiver);
}
+
+ private static String getServiceCacheKey(@NonNull android.content.pm.ServiceInfo serviceInfo) {
+ StringBuilder sb = new StringBuilder(serviceInfo.packageName);
+ sb.append('-');
+ sb.append(serviceInfo.name);
+ return sb.toString();
+ }
+
+ private ServiceInfo<V> getServiceInfoFromServiceCache(
+ @NonNull android.content.pm.ServiceInfo serviceInfo, long lastUpdateTime) {
+ String serviceCacheKey = getServiceCacheKey(serviceInfo);
+ synchronized (mServicesLock) {
+ ServiceInfo<V> serviceCache = mServiceInfoCaches.get(serviceCacheKey);
+ if (serviceCache == null) {
+ return null;
+ }
+ if (serviceCache.lastUpdateTime == lastUpdateTime) {
+ return serviceCache;
+ }
+ // The service is not latest, remove it from the cache.
+ mServiceInfoCaches.remove(serviceCacheKey);
+ return null;
+ }
+ }
}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 7bba06c..e4b8c90 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -383,3 +383,11 @@
bug: "334024639"
description: "Feature flag to check whether a given UID can access a content provider"
}
+
+flag {
+ name: "optimize_parsing_in_registered_services_cache"
+ namespace: "package_manager_service"
+ description: "Feature flag to optimize RegisteredServicesCache ServiceInfo parsing by using caches."
+ bug: "319137634"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java
index 2d3d252..868429c 100644
--- a/core/java/android/hardware/SystemSensorManager.java
+++ b/core/java/android/hardware/SystemSensorManager.java
@@ -16,8 +16,6 @@
package android.hardware;
-import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED;
-import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS;
import static android.content.Context.DEVICE_ID_DEFAULT;
@@ -164,11 +162,7 @@
// initialize the sensor list
for (int index = 0;; ++index) {
Sensor sensor = new Sensor();
- if (android.companion.virtual.flags.Flags.enableNativeVdm()) {
- if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
- } else {
- if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break;
- }
+ if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
mFullSensorsList.add(sensor);
mHandleToSensor.put(sensor.getHandle(), sensor);
}
@@ -555,11 +549,7 @@
}
private List<Sensor> createRuntimeSensorListLocked(int deviceId) {
- if (android.companion.virtual.flags.Flags.vdmPublicApis()) {
- setupVirtualDeviceListener();
- } else {
- setupRuntimeSensorBroadcastReceiver();
- }
+ setupVirtualDeviceListener();
List<Sensor> list = new ArrayList<>();
nativeGetRuntimeSensors(mNativeInstance, deviceId, list);
mFullRuntimeSensorListByDevice.put(deviceId, list);
@@ -570,35 +560,6 @@
return list;
}
- private void setupRuntimeSensorBroadcastReceiver() {
- if (mRuntimeSensorBroadcastReceiver == null) {
- mRuntimeSensorBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ACTION_VIRTUAL_DEVICE_REMOVED)) {
- synchronized (mFullRuntimeSensorListByDevice) {
- final int deviceId = intent.getIntExtra(
- EXTRA_VIRTUAL_DEVICE_ID, DEVICE_ID_DEFAULT);
- List<Sensor> removedSensors =
- mFullRuntimeSensorListByDevice.removeReturnOld(deviceId);
- if (removedSensors != null) {
- for (Sensor s : removedSensors) {
- cleanupSensorConnection(s);
- }
- }
- mRuntimeSensorListByDeviceByType.remove(deviceId);
- }
- }
- }
- };
-
- IntentFilter filter = new IntentFilter("virtual_device_removed");
- filter.addAction(ACTION_VIRTUAL_DEVICE_REMOVED);
- mContext.registerReceiver(mRuntimeSensorBroadcastReceiver, filter,
- Context.RECEIVER_NOT_EXPORTED);
- }
- }
-
private void setupVirtualDeviceListener() {
if (mVirtualDeviceListener != null) {
return;
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 0cd3209..9181bd0 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -697,6 +697,7 @@
*
* @param endpointId Statically generated ID for an endpoint.
* @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@@ -733,6 +734,7 @@
* cannot be null or empty.
* @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
* @throws IllegalArgumentException if the serviceDescriptor is empty/null.
+ * @throws UnsupportedOperationException If the operation is not supported.
*/
@FlaggedApi(Flags.FLAG_OFFLOAD_API)
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
diff --git a/core/java/android/os/CpuHeadroomParams.java b/core/java/android/os/CpuHeadroomParams.java
index 072c012..e511976 100644
--- a/core/java/android/os/CpuHeadroomParams.java
+++ b/core/java/android/os/CpuHeadroomParams.java
@@ -28,15 +28,11 @@
/**
* Headroom request params used by {@link SystemHealthManager#getCpuHeadroom(CpuHeadroomParams)}.
+ *
+ * <p>This class is immutable and one should use the {@link Builder} to build a new instance.
*/
@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
public final class CpuHeadroomParams {
- final CpuHeadroomParamsInternal mInternal;
-
- public CpuHeadroomParams() {
- mInternal = new CpuHeadroomParamsInternal();
- }
-
/** @hide */
@IntDef(flag = false, prefix = {"CPU_HEADROOM_CALCULATION_TYPE_"}, value = {
CPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
@@ -47,107 +43,188 @@
}
/**
- * Calculates the headroom based on minimum value over a device-defined window.
+ * The headroom calculation type bases on minimum value over a specified window.
*/
public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
/**
- * Calculates the headroom based on average value over a device-defined window.
+ * The headroom calculation type bases on average value over a specified window.
*/
public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
- private static final int CALCULATION_WINDOW_MILLIS_MIN = 50;
- private static final int CALCULATION_WINDOW_MILLIS_MAX = 10000;
- private static final int MAX_TID_COUNT = 5;
+ /** @hide */
+ public final CpuHeadroomParamsInternal mInternal;
+
+ private CpuHeadroomParams() {
+ mInternal = new CpuHeadroomParamsInternal();
+ }
+
+ public static final class Builder {
+ private int mCalculationType = -1;
+ private int mCalculationWindowMillis = -1;
+ private int[] mTids = null;
+
+ public Builder() {
+ }
+
+ /**
+ * Returns a new builder copy with the same values as the params.
+ */
+ public Builder(@NonNull CpuHeadroomParams params) {
+ if (params.mInternal.calculationType >= 0) {
+ mCalculationType = params.mInternal.calculationType;
+ }
+ if (params.mInternal.calculationWindowMillis >= 0) {
+ mCalculationWindowMillis = params.mInternal.calculationWindowMillis;
+ }
+ if (params.mInternal.tids != null) {
+ mTids = Arrays.copyOf(params.mInternal.tids, params.mInternal.tids.length);
+ }
+ }
+
+ /**
+ * Sets the headroom calculation type.
+ * <p>
+ *
+ * @throws IllegalArgumentException if the type is invalid.
+ */
+ @NonNull
+ public Builder setCalculationType(
+ @CpuHeadroomCalculationType int calculationType) {
+ switch (calculationType) {
+ case CPU_HEADROOM_CALCULATION_TYPE_MIN:
+ case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE: {
+ mCalculationType = calculationType;
+ return this;
+ }
+ }
+ throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+ }
+
+ /**
+ * Sets the headroom calculation window size in milliseconds.
+ * <p>
+ *
+ * @param windowMillis the window size in milliseconds ranges from
+ * {@link SystemHealthManager#getCpuHeadroomCalculationWindowRange()}.
+ * The smaller the window size, the larger fluctuation in the headroom
+ * value should be expected. The default value can be retrieved from
+ * the {@link CpuHeadroomParams#getCalculationWindowMillis}. The device
+ * will try to use the closest feasible window size to this param.
+ * @throws IllegalArgumentException if the window is invalid.
+ */
+ @NonNull
+ public Builder setCalculationWindowMillis(@IntRange(from = 1) int windowMillis) {
+ if (windowMillis <= 0) {
+ throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
+ }
+ mCalculationWindowMillis = windowMillis;
+ return this;
+ }
+
+ /**
+ * Sets the thread TIDs to track.
+ * <p>
+ * The TIDs should belong to the same of the process that will make the headroom call. And
+ * they should not have different core affinity.
+ * <p>
+ * If not set or set to empty, the headroom will be based on the PID of the process making
+ * the call.
+ *
+ * @param tids non-null list of TIDs, where maximum size can be read from
+ * {@link SystemHealthManager#getMaxCpuHeadroomTidsSize()}.
+ * @throws IllegalArgumentException if the TID is not positive.
+ */
+ @NonNull
+ public Builder setTids(@NonNull int... tids) {
+ for (int tid : tids) {
+ if (tid <= 0) {
+ throw new IllegalArgumentException("Invalid TID: " + tid);
+ }
+ }
+ mTids = tids;
+ return this;
+ }
+
+ /**
+ * Builds the {@link CpuHeadroomParams} object.
+ */
+ @NonNull
+ public CpuHeadroomParams build() {
+ CpuHeadroomParams params = new CpuHeadroomParams();
+ if (mCalculationType >= 0) {
+ params.mInternal.calculationType = (byte) mCalculationType;
+ }
+ if (mCalculationWindowMillis >= 0) {
+ params.mInternal.calculationWindowMillis = mCalculationWindowMillis;
+ }
+ if (mTids != null) {
+ params.mInternal.tids = mTids;
+ }
+ return params;
+ }
+ }
/**
- * Sets the headroom calculation type.
- * <p>
- *
- * @throws IllegalArgumentException if the type is invalid.
+ * Returns a new builder with the same values as this object.
*/
- public void setCalculationType(@CpuHeadroomCalculationType int calculationType) {
- switch (calculationType) {
- case CPU_HEADROOM_CALCULATION_TYPE_MIN:
- case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
- mInternal.calculationType = (byte) calculationType;
- return;
- }
- throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+ @NonNull
+ public Builder toBuilder() {
+ return new Builder(this);
}
/**
* Gets the headroom calculation type.
- * Default to {@link #CPU_HEADROOM_CALCULATION_TYPE_MIN} if not set.
+ * <p>
+ * This will return the default value chosen by the device if not set.
*/
public @CpuHeadroomCalculationType int getCalculationType() {
@CpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
- case CPU_HEADROOM_CALCULATION_TYPE_MIN, CPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
- mInternal.calculationType;
+ case CPU_HEADROOM_CALCULATION_TYPE_MIN,
+ CPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> mInternal.calculationType;
default -> CPU_HEADROOM_CALCULATION_TYPE_MIN;
};
return validatedType;
}
/**
- * Sets the headroom calculation window size in milliseconds.
- * <p>
- *
- * @param windowMillis the window size in milliseconds ranges from [50, 10000]. The smaller the
- * window size, the larger fluctuation in the headroom value should be
- * expected. The default value can be retrieved from the
- * {@link #getCalculationWindowMillis}. The device will try to use the
- * closest feasible window size to this param.
- * @throws IllegalArgumentException if the window size is not in allowed range.
- */
- public void setCalculationWindowMillis(
- @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) int windowMillis) {
- if (windowMillis < CALCULATION_WINDOW_MILLIS_MIN
- || windowMillis > CALCULATION_WINDOW_MILLIS_MAX) {
- throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
- }
- mInternal.calculationWindowMillis = windowMillis;
- }
-
- /**
* Gets the headroom calculation window size in milliseconds.
* <p>
- * This will return the default value chosen by the device if the params is not set.
+ * This will return the default value chosen by the device if not set.
*/
- public @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) long getCalculationWindowMillis() {
+ public long getCalculationWindowMillis() {
return mInternal.calculationWindowMillis;
}
/**
- * Sets the thread TIDs to track.
+ * Gets the TIDs to track.
* <p>
- * The TIDs should belong to the same of the process that will the headroom call. And they
- * should not have different core affinity.
- * <p>
- * If not set, the headroom will be based on the PID of the process making the call.
- *
- * @param tids non-empty list of TIDs, maximum 5.
- * @throws IllegalArgumentException if the list size is not in allowed range or TID is not
- * positive.
+ * This will return a copy of the TIDs in the params, or null if the params is not set.
*/
- public void setTids(@NonNull int... tids) {
- if (tids.length == 0 || tids.length > MAX_TID_COUNT) {
- throw new IllegalArgumentException("Invalid number of TIDs: " + tids.length);
- }
- for (int tid : tids) {
- if (tid <= 0) {
- throw new IllegalArgumentException("Invalid TID: " + tid);
- }
- }
- mInternal.tids = Arrays.copyOf(tids, tids.length);
+ @NonNull
+ public int[] getTids() {
+ return mInternal.tids == null ? null : Arrays.copyOf(mInternal.tids, mInternal.tids.length);
}
- /**
- * @hide
- */
- public CpuHeadroomParamsInternal getInternal() {
- return mInternal;
+ @Override
+ public String toString() {
+ return "CpuHeadroomParams{"
+ + "calculationType=" + mInternal.calculationType
+ + ", calculationWindowMillis=" + mInternal.calculationWindowMillis
+ + ", tids=" + Arrays.toString(mInternal.tids)
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ CpuHeadroomParams that = (CpuHeadroomParams) o;
+ return mInternal.equals(that.mInternal);
+ }
+
+ @Override
+ public int hashCode() {
+ return mInternal.hashCode();
}
}
diff --git a/core/java/android/os/GpuHeadroomParams.java b/core/java/android/os/GpuHeadroomParams.java
index 126ee8c..5c5e7bb 100644
--- a/core/java/android/os/GpuHeadroomParams.java
+++ b/core/java/android/os/GpuHeadroomParams.java
@@ -19,6 +19,7 @@
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
+import android.annotation.NonNull;
import android.os.health.SystemHealthManager;
import java.lang.annotation.Retention;
@@ -26,15 +27,11 @@
/**
* Headroom request params used by {@link SystemHealthManager#getGpuHeadroom(GpuHeadroomParams)}.
+ *
+ * <p>This class is immutable and one should use the {@link Builder} to build a new instance.
*/
@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS)
public final class GpuHeadroomParams {
- final GpuHeadroomParamsInternal mInternal;
-
- public GpuHeadroomParams() {
- mInternal = new GpuHeadroomParamsInternal();
- }
-
/** @hide */
@IntDef(flag = false, prefix = {"GPU_HEADROOM_CALCULATION_TYPE_"}, value = {
GPU_HEADROOM_CALCULATION_TYPE_MIN, // 0
@@ -45,82 +42,153 @@
}
/**
- * Calculates the headroom based on minimum value over a device-defined window.
+ * The headroom calculation type bases on minimum value over a specified window.
*/
public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0;
/**
- * Calculates the headroom based on average value over a device-defined window.
+ * The headroom calculation type bases on average value over a specified window.
*/
public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1;
- private static final int CALCULATION_WINDOW_MILLIS_MIN = 50;
- private static final int CALCULATION_WINDOW_MILLIS_MAX = 10000;
+ /**
+ * The minimum size of the window to compute the headroom over.
+ */
+ public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
/**
- * Sets the headroom calculation type.
- * <p>
- *
- * @throws IllegalArgumentException if the type is invalid.
+ * The maximum size of the window to compute the headroom over.
*/
- public void setCalculationType(@GpuHeadroomCalculationType int calculationType) {
- switch (calculationType) {
- case GPU_HEADROOM_CALCULATION_TYPE_MIN:
- case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE:
- mInternal.calculationType = (byte) calculationType;
- return;
+ public static final int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
+
+ /**
+ * @hide
+ */
+ public final GpuHeadroomParamsInternal mInternal;
+
+ /**
+ * @hide
+ */
+ private GpuHeadroomParams() {
+ mInternal = new GpuHeadroomParamsInternal();
+ }
+
+ public static final class Builder {
+ private int mCalculationType = -1;
+ private int mCalculationWindowMillis = -1;
+
+ public Builder() {
}
- throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+
+ /**
+ * Returns a new builder with the same values as this object.
+ */
+ public Builder(@NonNull GpuHeadroomParams params) {
+ if (params.mInternal.calculationType >= 0) {
+ mCalculationType = params.mInternal.calculationType;
+ }
+ if (params.mInternal.calculationWindowMillis >= 0) {
+ mCalculationWindowMillis = params.mInternal.calculationWindowMillis;
+ }
+ }
+
+ /**
+ * Sets the headroom calculation type.
+ * <p>
+ *
+ * @throws IllegalArgumentException if the type is invalid.
+ */
+ @NonNull
+ public Builder setCalculationType(
+ @GpuHeadroomCalculationType int calculationType) {
+ switch (calculationType) {
+ case GPU_HEADROOM_CALCULATION_TYPE_MIN:
+ case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE: {
+ mCalculationType = calculationType;
+ return this;
+ }
+ }
+ throw new IllegalArgumentException("Invalid calculation type: " + calculationType);
+ }
+
+ /**
+ * Sets the headroom calculation window size in milliseconds.
+ * <p>
+ *
+ * @param windowMillis the window size in milliseconds ranges from
+ * {@link SystemHealthManager#getGpuHeadroomCalculationWindowRange()}.
+ * The smaller the window size, the larger fluctuation in the headroom
+ * value should be expected. The default value can be retrieved from
+ * the {@link GpuHeadroomParams#getCalculationWindowMillis}. The device
+ * will try to use the closest feasible window size to this param.
+ * @throws IllegalArgumentException if the window is invalid.
+ */
+ @NonNull
+ public Builder setCalculationWindowMillis(@IntRange(from = 1) int windowMillis) {
+ if (windowMillis <= 0) {
+ throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
+ }
+ mCalculationWindowMillis = windowMillis;
+ return this;
+ }
+
+ /**
+ * Builds the {@link GpuHeadroomParams} object.
+ */
+ @NonNull
+ public GpuHeadroomParams build() {
+ GpuHeadroomParams params = new GpuHeadroomParams();
+ if (mCalculationType >= 0) {
+ params.mInternal.calculationType = (byte) mCalculationType;
+ }
+ if (mCalculationWindowMillis >= 0) {
+ params.mInternal.calculationWindowMillis = mCalculationWindowMillis;
+ }
+ return params;
+ }
}
/**
* Gets the headroom calculation type.
- * Default to {@link #GPU_HEADROOM_CALCULATION_TYPE_MIN} if the params is not set.
+ * <p>
+ * This will return the default value chosen by the device if not set.
*/
public @GpuHeadroomCalculationType int getCalculationType() {
@GpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) {
- case GPU_HEADROOM_CALCULATION_TYPE_MIN, GPU_HEADROOM_CALCULATION_TYPE_AVERAGE ->
- mInternal.calculationType;
+ case GPU_HEADROOM_CALCULATION_TYPE_MIN,
+ GPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> mInternal.calculationType;
default -> GPU_HEADROOM_CALCULATION_TYPE_MIN;
};
return validatedType;
}
/**
- * Sets the headroom calculation window size in milliseconds.
- * <p>
- *
- * @param windowMillis the window size in milliseconds ranges from [50, 10000]. The smaller the
- * window size, the larger fluctuation in the headroom value should be
- * expected. The default value can be retrieved from the
- * {@link #getCalculationWindowMillis}. The device will try to use the
- * closest feasible window size to this param.
- * @throws IllegalArgumentException if the window is invalid.
- */
- public void setCalculationWindowMillis(
- @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) int windowMillis) {
- if (windowMillis < CALCULATION_WINDOW_MILLIS_MIN
- || windowMillis > CALCULATION_WINDOW_MILLIS_MAX) {
- throw new IllegalArgumentException("Invalid calculation window: " + windowMillis);
- }
- mInternal.calculationWindowMillis = windowMillis;
- }
-
- /**
* Gets the headroom calculation window size in milliseconds.
* <p>
* This will return the default value chosen by the device if not set.
*/
- public @IntRange(from = CALCULATION_WINDOW_MILLIS_MIN, to =
- CALCULATION_WINDOW_MILLIS_MAX) int getCalculationWindowMillis() {
+ public int getCalculationWindowMillis() {
return mInternal.calculationWindowMillis;
}
- /**
- * @hide
- */
- public GpuHeadroomParamsInternal getInternal() {
- return mInternal;
+ @Override
+ public String toString() {
+ return "GpuHeadroomParams{"
+ + "calculationType=" + mInternal.calculationType
+ + ", calculationWindowMillis=" + mInternal.calculationWindowMillis
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ GpuHeadroomParams that = (GpuHeadroomParams) o;
+ return mInternal.equals(that.mInternal);
+ }
+
+ @Override
+ public int hashCode() {
+ return mInternal.hashCode();
}
}
diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl
index 5128e91..2432545 100644
--- a/core/java/android/os/IHintManager.aidl
+++ b/core/java/android/os/IHintManager.aidl
@@ -72,6 +72,7 @@
parcelable HintManagerClientData {
int powerHalVersion;
int maxGraphicsPipelineThreads;
+ int maxCpuHeadroomThreads;
long preferredRateNanos;
SupportInfo supportInfo;
}
@@ -88,4 +89,6 @@
* passing back a bundle of support and configuration information.
*/
HintManagerClientData registerClient(in IHintManagerClient client);
+
+ HintManagerClientData getClientData();
}
diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java
index cd79e41..a1e9cf2 100644
--- a/core/java/android/os/health/SystemHealthManager.java
+++ b/core/java/android/os/health/SystemHealthManager.java
@@ -18,6 +18,7 @@
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
@@ -42,6 +43,8 @@
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.SynchronousResultReceiver;
+import android.util.Pair;
+import android.util.Slog;
import com.android.internal.app.IBatteryStats;
import com.android.server.power.optimization.Flags;
@@ -69,15 +72,41 @@
* plugged in (e.g. using {@link android.app.job.JobScheduler JobScheduler}), and
* while that can affect charging rates, it is still preferable to actually draining
* the battery.
+ * <p>
+ * <b>CPU/GPU Usage</b><br>
+ * CPU/GPU headroom APIs are designed to be best used by applications with consistent and intense
+ * workload such as games to query the remaining capacity headroom over a short period and perform
+ * optimization accordingly. Due to the nature of the fast job scheduling and frequency scaling of
+ * CPU and GPU, the headroom by nature will have "TOCTOU" problem which makes it less suitable for
+ * apps with inconsistent or low workload to take any useful action but simply monitoring. And to
+ * avoid oscillation it's not recommended to adjust workload too frequent (on each polling request)
+ * or too aggressively. As the headroom calculation is more based on reflecting past history usage
+ * than predicting future capacity. Take game as an example, if the API returns CPU headroom of 0 in
+ * one scenario (especially if it's constant across multiple calls), or some value significantly
+ * smaller than other scenarios, then it can reason that the recent performance result is more CPU
+ * bottlenecked. Then reducing the CPU workload intensity can help reserve some headroom to handle
+ * the load variance better, which can result in less frame drops or smooth FPS value. On the other
+ * hand, if the API returns large CPU headroom constantly, the app can be more confident to increase
+ * the workload and expect higher possibility of device meeting its performance expectation.
+ * App can also use thermal APIs to read the current thermal status and headroom first, then poll
+ * the CPU and GPU headroom if the device is (about to) getting thermal throttled. If the CPU/GPU
+ * headrooms provide enough significance such as one valued at 0 while the other at 100, then it can
+ * be used to infer that reducing CPU workload could be more efficient to cool down the device.
+ * There is a caveat that the power controller may scale down the frequency of the CPU and GPU due
+ * to thermal and other reasons, which can result in a higher than usual percentage usage of the
+ * capacity.
*/
@SystemService(Context.SYSTEM_HEALTH_SERVICE)
public class SystemHealthManager {
+ private static final String TAG = "SystemHealthManager";
@NonNull
private final IBatteryStats mBatteryStats;
@Nullable
private final IPowerStatsService mPowerStats;
@Nullable
private final IHintManager mHintManager;
+ @Nullable
+ private final IHintManager.HintManagerClientData mHintManagerClientData;
private List<PowerMonitor> mPowerMonitorsInfo;
private final Object mPowerMonitorsLock = new Object();
private static final long TAKE_UID_SNAPSHOT_TIMEOUT_MILLIS = 10_000;
@@ -109,29 +138,72 @@
mBatteryStats = batteryStats;
mPowerStats = powerStats;
mHintManager = hintManager;
+ IHintManager.HintManagerClientData data = null;
+ if (mHintManager != null) {
+ try {
+ data = mHintManager.getClientData();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get hint manager client data", e);
+ }
+ }
+ mHintManagerClientData = data;
}
/**
- * Provides an estimate of global available CPU headroom.
+ * Provides an estimate of available CPU capacity headroom of the device.
* <p>
+ * The value can be used by the calling application to determine if the workload was CPU bound
+ * and then take action accordingly to ensure that the workload can be completed smoothly. It
+ * can also be used with the thermal status and headroom to determine if reducing the CPU bound
+ * workload can help reduce the device temperature to avoid thermal throttling.
+ * <p>
+ * If the params are valid, each call will perform at least one synchronous binder transaction
+ * that can take more than 1ms. So it's not recommended to call or wait for this on critical
+ * threads. Some devices may implement this as an on-demand API with lazy initialization, so the
+ * caller should expect higher latency when making the first call (especially with non-default
+ * params) since app starts or after changing params, as the device may need to change its data
+ * collection.
*
- * @param params params to customize the CPU headroom calculation, null to use default params.
- * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
- * A valid value is ranged from [0, 100], where 0 indicates no more CPU resources can be
- * granted.
+ * @param params params to customize the CPU headroom calculation, or null to use default.
+ * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable due to
+ * server error or not enough user CPU workload.
+ * Each valid value ranges from [0, 100], where 0 indicates no more cpu resources can be
+ * granted
* @throws UnsupportedOperationException if the API is unsupported.
+ * @throws IllegalArgumentException if the params are invalid.
* @throws SecurityException if the TIDs of the params don't belong to the same process.
* @throws IllegalStateException if the TIDs of the params don't have the same affinity setting.
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public @FloatRange(from = 0f, to = 100f) float getCpuHeadroom(
@Nullable CpuHeadroomParams params) {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
throw new UnsupportedOperationException();
}
+ if (params != null) {
+ if (params.mInternal.tids != null && (params.mInternal.tids.length == 0
+ || params.mInternal.tids.length
+ > mHintManagerClientData.maxCpuHeadroomThreads)) {
+ throw new IllegalArgumentException(
+ "Invalid number of TIDs: " + params.mInternal.tids.length);
+ }
+ if (params.mInternal.calculationWindowMillis
+ < mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis
+ || params.mInternal.calculationWindowMillis
+ > mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis) {
+ throw new IllegalArgumentException(
+ "Invalid calculation window: "
+ + params.mInternal.calculationWindowMillis + ", expect range: ["
+ + mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis
+ + ", "
+ + mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis
+ + "]");
+ }
+ }
try {
final CpuHeadroomResult ret = mHintManager.getCpuHeadroom(
- params != null ? params.getInternal() : new CpuHeadroomParamsInternal());
+ params != null ? params.mInternal : new CpuHeadroomParamsInternal());
if (ret == null || ret.getTag() != CpuHeadroomResult.globalHeadroom) {
return Float.NaN;
}
@@ -141,27 +213,69 @@
}
}
-
+ /**
+ * Gets the maximum number of TIDs this device supports for getting CPU headroom.
+ * <p>
+ * See {@link CpuHeadroomParams#setTids(int...)}.
+ *
+ * @return the maximum size of TIDs supported
+ * @throws UnsupportedOperationException if the CPU headroom API is unsupported.
+ */
+ @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+ public @IntRange(from = 1) int getMaxCpuHeadroomTidsSize() {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
+ throw new UnsupportedOperationException();
+ }
+ return mHintManagerClientData.maxCpuHeadroomThreads;
+ }
/**
- * Provides an estimate of global available GPU headroom of the device.
+ * Provides an estimate of available GPU capacity headroom of the device.
* <p>
+ * The value can be used by the calling application to determine if the workload was GPU bound
+ * and then take action accordingly to ensure that the workload can be completed smoothly. It
+ * can also be used with the thermal status and headroom to determine if reducing the GPU bound
+ * workload can help reduce the device temperature to avoid thermal throttling.
+ * <p>
+ * If the params are valid, each call will perform at least one synchronous binder transaction
+ * that can take more than 1ms. So it's not recommended to call or wait for this on critical
+ * threads. Some devices may implement this as an on-demand API with lazy initialization, so the
+ * caller should expect higher latency when making the first call (especially with non-default
+ * params) since app starts or after changing params, as the device may need to change its data
+ * collection.
*
- * @param params params to customize the GPU headroom calculation, null to use default params.
+ * @param params params to customize the GPU headroom calculation, or null to use default.
* @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable.
- * A valid value is ranged from [0, 100], where 0 indicates no more GPU resources can be
+ * Each valid value ranges from [0, 100], where 0 indicates no more cpu resources can be
* granted.
* @throws UnsupportedOperationException if the API is unsupported.
+ * @throws IllegalArgumentException if the params are invalid.
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public @FloatRange(from = 0f, to = 100f) float getGpuHeadroom(
@Nullable GpuHeadroomParams params) {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
throw new UnsupportedOperationException();
}
+ if (params != null) {
+ if (params.mInternal.calculationWindowMillis
+ < mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis
+ || params.mInternal.calculationWindowMillis
+ > mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis) {
+ throw new IllegalArgumentException(
+ "Invalid calculation window: "
+ + params.mInternal.calculationWindowMillis + ", expect range: ["
+ + mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis
+ + ", "
+ + mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis
+ + "]");
+ }
+ }
try {
final GpuHeadroomResult ret = mHintManager.getGpuHeadroom(
- params != null ? params.getInternal() : new GpuHeadroomParamsInternal());
+ params != null ? params.mInternal : new GpuHeadroomParamsInternal());
if (ret == null || ret.getTag() != GpuHeadroomResult.globalHeadroom) {
return Float.NaN;
}
@@ -172,7 +286,51 @@
}
/**
- * Minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
+ * Gets the range of the calculation window size for CPU headroom.
+ * <p>
+ * In API version 36, the range will be a superset of [50, 10000].
+ * <p>
+ * See {@link CpuHeadroomParams#setCalculationWindowMillis(int)}.
+ *
+ * @return the range of the calculation window size supported in milliseconds.
+ * @throws UnsupportedOperationException if the CPU headroom API is unsupported.
+ */
+ @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+ @NonNull
+ public Pair<Integer, Integer> getCpuHeadroomCalculationWindowRange() {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
+ throw new UnsupportedOperationException();
+ }
+ return new Pair<>(
+ mHintManagerClientData.supportInfo.headroom.cpuMinCalculationWindowMillis,
+ mHintManagerClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis);
+ }
+
+ /**
+ * Gets the range of the calculation window size for GPU headroom.
+ * <p>
+ * In API version 36, the range will be a superset of [50, 10000].
+ * <p>
+ * See {@link GpuHeadroomParams#setCalculationWindowMillis(int)}.
+ *
+ * @return the range of the calculation window size supported in milliseconds.
+ * @throws UnsupportedOperationException if the GPU headroom API is unsupported.
+ */
+ @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
+ @NonNull
+ public Pair<Integer, Integer> getGpuHeadroomCalculationWindowRange() {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
+ throw new UnsupportedOperationException();
+ }
+ return new Pair<>(
+ mHintManagerClientData.supportInfo.headroom.gpuMinCalculationWindowMillis,
+ mHintManagerClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis);
+ }
+
+ /**
+ * Gets minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in
* milliseconds.
* <p>
* The {@link #getCpuHeadroom(CpuHeadroomParams)} API may return cached result if called more
@@ -182,7 +340,8 @@
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public long getCpuHeadroomMinIntervalMillis() {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) {
throw new UnsupportedOperationException();
}
try {
@@ -193,7 +352,7 @@
}
/**
- * Minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
+ * Gets minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in
* milliseconds.
* <p>
* The {@link #getGpuHeadroom(GpuHeadroomParams)} API may return cached result if called more
@@ -203,7 +362,8 @@
*/
@FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS)
public long getGpuHeadroomMinIntervalMillis() {
- if (mHintManager == null) {
+ if (mHintManager == null || mHintManagerClientData == null
+ || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) {
throw new UnsupportedOperationException();
}
try {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index df12a68..c94526b 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -12223,49 +12223,6 @@
"back_gesture_inset_scale_right";
/**
- * Indicates whether the trackpad back gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_BACK_ENABLED = "trackpad_gesture_back_enabled";
-
- /**
- * Indicates whether the trackpad home gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_HOME_ENABLED = "trackpad_gesture_home_enabled";
-
- /**
- * Indicates whether the trackpad overview gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_OVERVIEW_ENABLED =
- "trackpad_gesture_overview_enabled";
-
- /**
- * Indicates whether the trackpad notification gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_NOTIFICATION_ENABLED =
- "trackpad_gesture_notification_enabled";
-
- /**
- * Indicates whether the trackpad quick switch gesture is enabled.
- * <p>Type: int (0 for false, 1 for true)
- *
- * @hide
- */
- public static final String TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED =
- "trackpad_gesture_quick_switch_enabled";
-
- /**
* Current provider of proximity-based sharing services.
* Default value in @string/config_defaultNearbySharingComponent.
* No VALIDATOR as this setting will not be backed up.
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index d8a88b8..d313fc9 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -100,13 +100,6 @@
public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD = "settings_new_keyboard_trackpad";
/**
- * Enable trackpad gesture settings UI
- * @hide
- */
- public static final String SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE =
- "settings_new_keyboard_trackpad_gesture";
-
- /**
* Enable the new pages which is implemented with SPA.
* @hide
*/
@@ -211,7 +204,6 @@
DEFAULT_FLAGS.put(SETTINGS_NEED_CONNECTED_BLE_DEVICE_FOR_BROADCAST, "true");
DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true");
DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true");
- DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "true");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_PHASE2, "false");
DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_METRICS, "true");
@@ -237,7 +229,6 @@
PERSISTENT_FLAGS.add(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME);
PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD);
- PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE);
PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA);
PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA_PHASE2);
PERSISTENT_FLAGS.add(SETTINGS_PREFER_ACCESSIBILITY_MENU_IN_SYSTEM);
diff --git a/core/java/android/util/SystemPropertySetter.java b/core/java/android/util/SystemPropertySetter.java
new file mode 100644
index 0000000..bf18f75
--- /dev/null
+++ b/core/java/android/util/SystemPropertySetter.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemProperties;
+
+/**
+ * This class provides a single, static function to set a system property. The function retries on
+ * error. System properties should be reliable but there have been reports of failures in the set()
+ * command on lower-end devices. Clients may want to use this method instead of calling
+ * {@link SystemProperties.set} directly.
+ * @hide
+ */
+public class SystemPropertySetter {
+
+ /**
+ * The default retryDelayMs for {@link #setWithRetry}. This value has been found to give
+ * reasonable behavior in the field.
+ */
+ public static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
+
+ /**
+ * The default retryLimit for {@link #setWithRetry}. This value has been found to give
+ * reasonable behavior in the field.
+ */
+ public static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
+
+ /**
+ * Set the value for the given {@code key} to {@code val}. This method retries using the
+ * standard parameters, above, if the native method throws a RuntimeException.
+ *
+ * @param key The name of the property to be set.
+ * @param val The new string value of the property.
+ * @throws IllegalArgumentException for non read-only properties if the {@code val} exceeds
+ * 91 characters.
+ * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+ * SELinux. libc will log the underlying reason.
+ */
+ public static void setWithRetry(@NonNull String key, @Nullable String val) {
+ setWithRetry(key, val,PROPERTY_FAILURE_RETRY_DELAY_MILLIS, PROPERTY_FAILURE_RETRY_LIMIT);
+ }
+
+ /**
+ * Set the value for the given {@code key} to {@code val}. This method retries if the native
+ * method throws a RuntimeException. If the {@code maxRetry} count is exceeded, the method
+ * throws the first RuntimeException that was seen.
+ *
+ * @param key The name of the property to be set.
+ * @param val The new string value of the property.
+ * @param maxRetry The maximum number of times; must be non-negative.
+ * @param retryDelayMs The number of milliseconds to wait between retries; must be positive.
+ * @throws IllegalArgumentException for non read-only properties if the {@code val} exceeds
+ * 91 characters, or if the retry parameters are invalid.
+ * @throws RuntimeException if the property cannot be set, for example, if it was blocked by
+ * SELinux. libc will log the underlying reason.
+ */
+ public static void setWithRetry(@NonNull String key, @Nullable String val, int maxRetry,
+ long retryDelayMs) {
+ if (maxRetry < 0) {
+ throw new IllegalArgumentException("invalid retry count: " + maxRetry);
+ }
+ if (retryDelayMs <= 0) {
+ throw new IllegalArgumentException("invalid retry delay: " + retryDelayMs);
+ }
+
+ RuntimeException failure = null;
+ for (int attempt = 0; attempt < maxRetry; attempt++) {
+ try {
+ SystemProperties.set(key, val);
+ return;
+ } catch (RuntimeException e) {
+ if (failure == null) {
+ failure = e;
+ }
+ try {
+ Thread.sleep(retryDelayMs);
+ } catch (InterruptedException x) {
+ // Ignore this exception. The desired delay is only approximate and
+ // there is no issue if the sleep sometimes terminates early.
+ }
+ }
+ }
+ // This point is reached only if SystemProperties.set() fails at least once.
+ // Rethrow the first exception that was received.
+ throw failure;
+ }
+}
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 19c98a2..cdfa7c81 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -34,6 +34,9 @@
/**
* Interface to control windows that generate insets.
+ *
+ * For guidance, see <a href="https://developer.android.com/develop/ui/views/layout/immersive">
+ * Hide system bars for immersive mode</a>.
*/
public interface WindowInsetsController {
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 617e476..f50ea91 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -50,7 +50,6 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.policy.PhoneWindow;
import com.android.internal.util.FastPrintWriter;
import java.io.FileDescriptor;
@@ -376,8 +375,7 @@
if (context != null && wparams.type > LAST_APPLICATION_WINDOW) {
final TypedArray styles = context.obtainStyledAttributes(R.styleable.Window);
- if (PhoneWindow.isOptingOutEdgeToEdgeEnforcement(
- context.getApplicationInfo(), true /* local */, styles)) {
+ if (styles.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)) {
wparams.privateFlags |= PRIVATE_FLAG_OPT_OUT_EDGE_TO_EDGE;
}
styles.recycle();
diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
index b3bd92b..c871d56 100644
--- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
+++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig
@@ -45,3 +45,10 @@
description: "If true, the APIs to manage content protection device policy will be enabled."
bug: "319477846"
}
+
+flag {
+ name: "exported_settings_activity_enabled"
+ namespace: "content_protection"
+ description: "If true, the content protection Settings Activity will be exported for launching externally."
+ bug: "385310141"
+}
diff --git a/core/java/android/widget/flags/flags.aconfig b/core/java/android/widget/flags/flags.aconfig
index 88eb043..83a4c86 100644
--- a/core/java/android/widget/flags/flags.aconfig
+++ b/core/java/android/widget/flags/flags.aconfig
@@ -2,7 +2,7 @@
container: "system"
flag {
name: "enable_fading_view_group"
- namespace: "system_performance"
+ namespace: "wear_systems"
description: "FRP screen during OOBE must have fading and scaling animation in Wear Watches"
bug: "348515581"
metadata {
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 091f86e..a864132 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -423,7 +423,7 @@
flag {
name: "port_window_size_animation"
- namespace: "systemui"
+ namespace: "windowing_frontend"
description: "Port window-resize animation from legacy to shell"
bug: "384976265"
}
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index fc41537..d1adfc9 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -246,8 +246,19 @@
*/
public static final int CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW = 119;
+ /**
+ * Track moving overview task to desktop interaction from overview menu.
+ *
+ * <p> Tracking starts when the overview task is moved to desktop via the overview menu.
+ * Tracking finishes when successfully made a call to `IDesktopMode.moveToDesktop`,
+ * without waiting for transition completion.
+ * </p>
+ * TODO(b/387471509): Update the CUJ to wait for transition completion.
+ */
+ public static final int CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU = 120;
+
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
- @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW;
+ @VisibleForTesting static final int LAST_CUJ = CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU;
/** @hide */
@IntDef({
@@ -358,7 +369,8 @@
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE,
CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
CUJ_DESKTOP_MODE_SNAP_RESIZE,
- CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW
+ CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW,
+ CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {}
@@ -480,6 +492,7 @@
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_SNAP_RESIZE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_SNAP_RESIZE;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_UNMAXIMIZE_WINDOW;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU;
}
private Cuj() {
@@ -714,6 +727,8 @@
return "DESKTOP_MODE_SNAP_RESIZE";
case CUJ_DESKTOP_MODE_UNMAXIMIZE_WINDOW:
return "DESKTOP_MODE_UNMAXIMIZE_WINDOW";
+ case CUJ_DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU:
+ return "DESKTOP_MODE_ENTER_FROM_OVERVIEW_MENU";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 67e358a4..e0529b3 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -184,14 +184,6 @@
private static final long ENFORCE_EDGE_TO_EDGE = 309578419;
/**
- * Disable opting out the edge-to-edge enforcement.
- * {@link Build.VERSION_CODES#BAKLAVA} or above.
- */
- @ChangeId
- @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
- private static final long DISABLE_OPT_OUT_EDGE_TO_EDGE = 377864165;
-
- /**
* Override the layout in display cutout mode behavior. This will only apply if the edge to edge
* is not enforced.
*/
@@ -458,7 +450,7 @@
*/
public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local,
TypedArray windowStyle) {
- return !isOptingOutEdgeToEdgeEnforcement(info, local, windowStyle)
+ return !windowStyle.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false)
&& (info.targetSdkVersion >= ENFORCE_EDGE_TO_EDGE_SDK_VERSION
|| (Flags.enforceEdgeToEdge() && (local
// Calling this doesn't require a permission.
@@ -467,26 +459,6 @@
: info.isChangeEnabled(ENFORCE_EDGE_TO_EDGE))));
}
- /**
- * Returns whether the given application is opting out edge-to-edge enforcement.
- *
- * @param info The application to query.
- * @param local Whether this is called from the process of the given application.
- * @param windowStyle The style of the window.
- * @return {@code true} if the edge-to-edge enforcement is opting out. Otherwise, {@code false}.
- */
- public static boolean isOptingOutEdgeToEdgeEnforcement(ApplicationInfo info, boolean local,
- TypedArray windowStyle) {
- final boolean disabled = (Flags.disableOptOutEdgeToEdge() && (local
- // Calling this doesn't require a permission.
- ? CompatChanges.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE)
- // Calling this requires permissions.
- : info.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE)));
- return !disabled && windowStyle.getBoolean(
- R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false /* default */);
-
- }
-
@Override
public final void setContainer(Window container) {
super.setContainer(container);
@@ -2514,7 +2486,6 @@
TypedArray a = getWindowStyle();
WindowManager.LayoutParams params = getAttributes();
- ApplicationInfo appInfo = getContext().getApplicationInfo();
if (false) {
System.out.println("From style:");
@@ -2526,7 +2497,8 @@
System.out.println(s);
}
- mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(appInfo, true /* local */, a);
+ mEdgeToEdgeEnforced = isEdgeToEdgeEnforced(
+ getContext().getApplicationInfo(), true /* local */, a);
if (mEdgeToEdgeEnforced) {
getAttributes().privateFlags |= PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
mDecorFitsSystemWindows = false;
@@ -2535,7 +2507,8 @@
// mNavigationBarColor is not reset here because it might be used to draw the scrim.
}
if (CompatChanges.isChangeEnabled(OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE)
- && !isOptingOutEdgeToEdgeEnforcement(appInfo, true /* local */, a)) {
+ && !a.getBoolean(R.styleable.Window_windowOptOutEdgeToEdgeEnforcement,
+ false /* defValue */)) {
getAttributes().privateFlags |= PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
}
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 8df3f2a..e522b50 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -94,14 +94,21 @@
/** Used for testing */
@Disabled
- @ChangeId public static final long TEST_COMPAT_ID_2 = 368131701L;
+ @ChangeId
+ public static final long TEST_COMPAT_ID_2 = 368131701L;
/** Used for testing */
@EnabledAfter(targetSdkVersion = S)
- @ChangeId public static final long TEST_COMPAT_ID_3 = 368131659L;
+ @ChangeId
+ public static final long TEST_COMPAT_ID_3 = 368131659L;
/** Used for testing */
@EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE)
- @ChangeId public static final long TEST_COMPAT_ID_4 = 368132057L;
+ @ChangeId
+ public static final long TEST_COMPAT_ID_4 = 368132057L;
+
+ /** Used for testing */
+ @ChangeId
+ public static final long TEST_COMPAT_ID_5 = 387558811L;
}
}
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 57bfc70..b2649a4 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -198,7 +198,7 @@
auto assetmanager = LockAndStartAssetManager(ptr);
const ScopedUtfChars package_name_utf8(env, package_name);
CHECK(package_name_utf8.c_str() != nullptr);
- const std::string std_package_name(package_name_utf8.c_str());
+ const std::string_view std_package_name(package_name_utf8.c_str());
const std::unordered_map<std::string, std::string>* map = nullptr;
assetmanager->ForEachPackage([&](const std::string& this_package_name, uint8_t package_id) {
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index cf81ba1..96d34a0 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -645,14 +645,8 @@
optional SettingProto theme_customization_overlay_packages = 75 [ (android.privacy).dest = DEST_AUTOMATIC ];
optional SettingProto trust_agents_initialized = 57 [ (android.privacy).dest = DEST_AUTOMATIC ];
- message TrackpadGesture {
- optional SettingProto trackpad_gesture_back_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_home_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_overview_enabled = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_notification_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ];
- optional SettingProto trackpad_gesture_quick_switch_enabled = 5 [ (android.privacy).dest = DEST_AUTOMATIC ];
- }
- optional TrackpadGesture trackpad_gesture = 94;
+ reserved 94; // formerly trackpad_gesture
+ reserved "trackpad_gesture";
message Tts {
option (android.msg_privacy).dest = DEST_EXPLICIT;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index dc95471..ed021b6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5405,10 +5405,9 @@
<permission android:name="android.permission.CHANGE_ACCESSIBILITY_VOLUME"
android:protectionLevel="signature" />
- <!-- @FlaggedApi("com.android.server.accessibility.motion_event_observing")
- @hide
- @TestApi
- Allows an accessibility service to observe motion events without consuming them. -->
+ <!-- @TestApi Allows an accessibility service to observe motion events
+ without consuming them.
+ @hide -->
<permission android:name="android.permission.ACCESSIBILITY_MOTION_EVENT_OBSERVING"
android:protectionLevel="signature" />
diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml
index c8263c2..1c36754 100644
--- a/core/res/res/layout/notification_2025_expand_button.xml
+++ b/core/res/res/layout/notification_2025_expand_button.xml
@@ -22,6 +22,7 @@
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:contentDescription="@string/expand_button_content_description_collapsed"
+ android:padding="@dimen/notification_2025_margin"
>
<LinearLayout
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index c827dcb..f108ce5 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -168,7 +168,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml
index ce38c164..6f3c15a 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_call.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml
@@ -70,7 +70,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 0021b83..bd17a3a 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -189,7 +189,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index f3e4ce1..edbebb1 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -193,7 +193,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_conversation.xml b/core/res/res/layout/notification_2025_template_conversation.xml
index 6be5a1c..24b6ad0 100644
--- a/core/res/res/layout/notification_2025_template_conversation.xml
+++ b/core/res/res/layout/notification_2025_template_conversation.xml
@@ -152,7 +152,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
/>
</LinearLayout>
</LinearLayout>
diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml
index 3f34eb3..0c07053 100644
--- a/core/res/res/layout/notification_2025_template_header.xml
+++ b/core/res/res/layout/notification_2025_template_header.xml
@@ -85,7 +85,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
- android:layout_margin="@dimen/notification_2025_margin"
android:layout_alignParentEnd="true" />
<include layout="@layout/notification_close_button"
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
index bb5380e..06cd44e 100644
--- a/core/res/res/xml/sms_short_codes.xml
+++ b/core/res/res/xml/sms_short_codes.xml
@@ -34,7 +34,7 @@
http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
<!-- Arab Emirates -->
- <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568" />
+ <shortcode country="ae" pattern="\\d{1,5}" free="1017|1355|3214|6253|6568|999" />
<!-- Albania: 5 digits, known short codes listed -->
<shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
@@ -63,8 +63,8 @@
<!-- Burkina Faso: 1-4 digits (standard system default, not country specific) -->
<shortcode country="bf" pattern="\\d{1,4}" free="3558" />
- <!-- Bulgaria: 4-5 digits, plus EU -->
- <shortcode country="bg" pattern="\\d{4,5}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490" />
+ <!-- Bulgaria: 4-6 digits, plus EU -->
+ <shortcode country="bg" pattern="\\d{4,6}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}|1988|1490|162055" />
<!-- Bahrain: 1-5 digits (standard system default, not country specific) -->
<shortcode country="bh" pattern="\\d{1,5}" free="81181|85999" />
@@ -81,18 +81,21 @@
<!-- Canada: 5-6 digits -->
<shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188|43030" standard="244444" free="455677|24470" />
+ <!-- DR Congo: 1-6 digits, known premium codes listed -->
+ <shortcode country="cd" pattern="\\d{1,6}" free="444123" />
+
<!-- Switzerland: 3-5 digits: http://www.swisscom.ch/fxres/kmu/thirdpartybusiness_code_of_conduct_en.pdf -->
<shortcode country="ch" pattern="[2-9]\\d{2,4}" premium="543|83111|30118" free="98765|30075|30047" />
<!-- Chile: 4-5 digits (not confirmed), known premium codes listed -->
- <shortcode country="cl" pattern="\\d{4,5}" free="9963|9240|1038" />
+ <shortcode country="cl" pattern="\\d{4,5}" free="9963|9240|1038|4848" />
<!-- China: premium shortcodes start with "1066", free shortcodes start with "1065":
http://clients.txtnation.com/entries/197192-china-premium-sms-short-code-requirements -->
<shortcode country="cn" premium="1066.*" free="1065.*" />
<!-- Colombia: 1-6 digits (not confirmed) -->
- <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289" />
+ <shortcode country="co" pattern="\\d{1,6}" free="890350|908160|892255|898002|898880|899960|899948|87739|85517|491289|890119" />
<!-- Costa Rica -->
<shortcode country="cr" pattern="\\d{1,6}" free="466453" />
@@ -116,6 +119,9 @@
<!-- Dominican Republic: 1-6 digits (standard system default, not country specific) -->
<shortcode country="do" pattern="\\d{1,6}" free="912892|912" />
+ <!-- Algeria: 1-5 digits, known premium codes listed -->
+ <shortcode country="dz" pattern="\\d{1,5}" free="63071" />
+
<!-- Ecuador: 1-6 digits (standard system default, not country specific) -->
<shortcode country="ec" pattern="\\d{1,6}" free="466453|18512" />
@@ -123,20 +129,23 @@
http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
<shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}|95034" />
- <!-- Egypt: 4-5 digits, known codes listed -->
- <shortcode country="eg" pattern="\\d{4,5}" free="1499|10020" />
+ <!-- Egypt: 4-6 digits, known codes listed -->
+ <shortcode country="eg" pattern="\\d{4,6}" free="1499|10020|100158" />
<!-- Spain: 5-6 digits: 25xxx, 27xxx, 280xx, 35xxx, 37xxx, 795xxx, 797xxx, 995xxx, 997xxx, plus EU.
http://www.legallink.es/?q=en/content/which-current-regulatory-status-premium-rate-services-spain -->
<shortcode country="es" premium="[23][57]\\d{3}|280\\d{2}|[79]9[57]\\d{3}" free="116\\d{3}|22791|222145|22189" />
+ <!-- Ethiopia: 1-4 digits, known codes listed -->
+ <shortcode country="et" pattern="\\d{1,4}" free="8527" />
+
<!-- Finland: 5-6 digits, premium 0600, 0700: http://en.wikipedia.org/wiki/Telephone_numbers_in_Finland -->
<shortcode country="fi" pattern="\\d{5,6}" premium="0600.*|0700.*|171(?:59|63)" free="116\\d{3}|14789|17110" />
<!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU:
http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements,
visual voicemail code for Orange: 21101 -->
- <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033" />
+ <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}|21101|20366|555|2051|33033|21727" />
<!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU:
http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf,
@@ -179,17 +188,17 @@
<shortcode country="il" pattern="\\d{1,5}" premium="4422|4545" free="37477|6681" />
<!-- Iran: 4-8 digits, known premium codes listed -->
- <shortcode country="ir" pattern="\\d{4,8}" free="700791|700792|100016|30008360" />
+ <shortcode country="ir" pattern="\\d{4,8}" free="700792|100016|30008360" />
<!-- Italy: 5 digits (premium=41xxx,42xxx), plus EU:
https://www.itu.int/dms_pub/itu-t/oth/02/02/T020200006B0001PDFE.pdf -->
<shortcode country="it" pattern="\\d{5}" premium="44[0-4]\\d{2}|47[0-4]\\d{2}|48[0-4]\\d{2}|44[5-9]\\d{4}|47[5-9]\\d{4}|48[5-9]\\d{4}|455\\d{2}|499\\d{2}" free="116\\d{3}|4112503|40\\d{0,12}" standard="430\\d{2}|431\\d{2}|434\\d{4}|435\\d{4}|439\\d{7}" />
<!-- Jordan: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="jo" pattern="\\d{1,5}" free="99066" />
+ <shortcode country="jo" pattern="\\d{1,5}" free="99066|99390" />
<!-- Japan: 8083 used by SOFTBANK_DCB_2 -->
- <shortcode country="jp" pattern="\\d{1,5}" free="8083" />
+ <shortcode country="jp" pattern="\\d{1,9}" free="8083|00050320" />
<!-- Kenya: 5 digits, known premium codes listed -->
<shortcode country="ke" pattern="\\d{5}" free="21725|21562|40520|23342|40023|24088|23054" />
@@ -206,6 +215,9 @@
<!-- Kuwait: 1-5 digits (standard system default, not country specific) -->
<shortcode country="kw" pattern="\\d{1,5}" free="1378|50420|94006|55991|50976|7112" />
+ <!-- Lesotho: 4-5 digits, known codes listed -->
+ <shortcode country="ls" pattern="\\d{4,5}" free="32012" />
+
<!-- Lithuania: 3-5 digits, known premium codes listed, plus EU -->
<shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}|1399|1324" />
@@ -222,11 +234,14 @@
<!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed -->
<shortcode country="mk" pattern="\\d{1,6}" free="129005|122" />
+ <!-- Mali: 1-5 digits, known codes listed -->
+ <shortcode country="ml" pattern="\\d{1,5}" free="36098" />
+
<!-- Mongolia : 1-6 digits (standard system default, not country specific) -->
<shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" />
<!-- Malawi: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305" />
+ <shortcode country="mw" pattern="\\d{1,5}" free="4276|4305|4326" />
<!-- Mozambique: 1-5 digits (standard system default, not country specific) -->
<shortcode country="mz" pattern="\\d{1,5}" free="1714" />
@@ -323,11 +338,14 @@
<!-- Tajikistan: 4 digits, known premium codes listed -->
<shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
- <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
- <shortcode country="tz" pattern="\\d{1,5}" free="15046|15234|15324|15610" />
+ <!-- Timor-Leste 1-5 digits, known codes listed -->
+ <shortcode country="tl" pattern="\\d{1,5}" free="46645" />
- <!-- Tunisia: 5 digits, known premium codes listed -->
- <shortcode country="tn" pattern="\\d{5}" free="85799" />
+ <!-- Tanzania: 1-5 digits (standard system default, not country specific) -->
+ <shortcode country="tz" pattern="\\d{1,5}" free="15046|15324|15610" />
+
+ <!-- Tunisia: 1-6 digits, known premium codes listed -->
+ <shortcode country="tn" pattern="\\d{1,6}" free="85799|772024" />
<!-- Turkey -->
<shortcode country="tr" pattern="\\d{1,5}" free="7529|5528|6493|3193" />
@@ -336,7 +354,7 @@
<shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
<!-- Uganda(UG): 4 digits (standard system default, not country specific) -->
- <shortcode country="ug" pattern="\\d{4}" free="8000|8009" />
+ <shortcode country="ug" pattern="\\d{4}" free="8009" />
<!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm),
visual voicemail code for T-Mobile: 122 -->
@@ -349,7 +367,7 @@
<shortcode country="ve" pattern="\\d{1,6}" free="538352" />
<!-- Vietnam: 1-6 digits (standard system default, not country specific) -->
- <shortcode country="vn" pattern="\\d{1,6}" free="5001|9055|8079|90002|118989" />
+ <shortcode country="vn" pattern="\\d{1,6}" free="5001|9055|90002|118989|46645" />
<!-- Mayotte (French Territory): 1-5 digits (not confirmed) -->
<shortcode country="yt" pattern="\\d{1,5}" free="38600,36300,36303,959" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 3bbb951..1b6746c 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -313,6 +313,7 @@
"res/xml/power_profile_test_modem.xml",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
test_module_config {
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index 37ef6cb..939bf2e 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -207,7 +207,8 @@
final ComponentInfo info = new ComponentInfo();
info.applicationInfo = new ApplicationInfo();
info.applicationInfo.uid = uid;
- return new RegisteredServicesCache.ServiceInfo<>(type, info, null);
+ return new RegisteredServicesCache.ServiceInfo<>(type, info, null /* componentName */,
+ 0 /* lastUpdateTime */);
}
private void assertNotEmptyFileCreated(TestServicesCache cache, int userId) {
@@ -301,7 +302,7 @@
@Override
protected ServiceInfo<TestServiceType> parseServiceInfo(
- ResolveInfo resolveInfo) throws XmlPullParserException, IOException {
+ ResolveInfo resolveInfo, int userId) throws XmlPullParserException, IOException {
int size = mServices.size();
for (int i = 0; i < size; i++) {
Map<ResolveInfo, ServiceInfo<TestServiceType>> map = mServices.valueAt(i);
diff --git a/core/tests/coretests/src/android/security/advancedprotection/OWNERS b/core/tests/coretests/src/android/security/advancedprotection/OWNERS
new file mode 100644
index 0000000..9bf5e58
--- /dev/null
+++ b/core/tests/coretests/src/android/security/advancedprotection/OWNERS
@@ -0,0 +1 @@
+file:platform/frameworks/base:main:/core/java/android/security/advancedprotection/OWNERS
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
index 5613caf..d26bb35 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java
@@ -120,7 +120,9 @@
List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
segments, points, progress, progressMax, isStyledByProgress);
- int fadedRed = 0x7FFF0000;
+ // Colors with 40% opacity
+ int fadedRed = 0x66FF0000;
+
List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true)));
assertThat(parts).isEqualTo(expected);
@@ -199,8 +201,8 @@
List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
segments, points, progress, progressMax, isStyledByProgress);
- // Colors with 50% opacity
- int fadedGreen = 0x7F00FF00;
+ // Colors with 40% opacity
+ int fadedGreen = 0x6600FF00;
List<Part> expected = new ArrayList<>(List.of(
new Segment(0.50f, Color.RED),
@@ -223,9 +225,9 @@
int progressMax = 100;
boolean isStyledByProgress = true;
- // Colors with 50% opacity
- int fadedBlue = 0x7F0000FF;
- int fadedYellow = 0x7FFFFF00;
+ // Colors with 40% opacity
+ int fadedBlue = 0x660000FF;
+ int fadedYellow = 0x66FFFF00;
List<Part> expected = new ArrayList<>(List.of(
new Segment(0.15f, Color.BLUE),
@@ -261,9 +263,9 @@
List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts(
segments, points, progress, progressMax, isStyledByProgress);
- // Colors with 50% opacity
- int fadedGreen = 0x7F00FF00;
- int fadedYellow = 0x7FFFFF00;
+ // Colors with 40% opacity
+ int fadedGreen = 0x6600FF00;
+ int fadedYellow = 0x66FFFF00;
List<Part> expected = new ArrayList<>(List.of(
new Segment(0.15f, Color.RED),
diff --git a/core/tests/systemproperties/Android.bp b/core/tests/systemproperties/Android.bp
index ed99a1f..9197dec 100644
--- a/core/tests/systemproperties/Android.bp
+++ b/core/tests/systemproperties/Android.bp
@@ -44,4 +44,5 @@
"src/**/*.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index 7cf49ab..5011f7a 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -69,4 +69,5 @@
"src/com/android/internal/util/**/*.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index eecf199..03076c0 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -35,7 +35,6 @@
android_robolectric_test {
name: "WMShellRobolectricTests",
instrumentation_for: "WindowManagerShellRobolectric",
- upstream: true,
java_resource_dirs: [
"robolectric/config",
],
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index 4300e84..2ca011b 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -16,10 +16,12 @@
package com.android.wm.shell.shared;
+import static android.app.WindowConfiguration.windowingModeToString;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+
import android.annotation.IntDef;
import android.app.ActivityManager.RecentTaskInfo;
import android.app.TaskInfo;
-import android.app.WindowConfiguration;
import android.os.Parcel;
import android.os.Parcelable;
@@ -28,11 +30,14 @@
import com.android.wm.shell.shared.split.SplitBounds;
+import kotlin.collections.CollectionsKt;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Simple container for recent tasks which should be presented as a single task within the
@@ -43,11 +48,13 @@
public static final int TYPE_FULLSCREEN = 1;
public static final int TYPE_SPLIT = 2;
public static final int TYPE_FREEFORM = 3;
+ public static final int TYPE_MIXED = 4;
@IntDef(prefix = {"TYPE_"}, value = {
TYPE_FULLSCREEN,
TYPE_SPLIT,
- TYPE_FREEFORM
+ TYPE_FREEFORM,
+ TYPE_MIXED
})
public @interface GroupType {}
@@ -64,7 +71,7 @@
* TYPE_SPLIT: Contains the two split roots of each side
* TYPE_FREEFORM: Contains the set of tasks currently in freeform mode
*/
- @NonNull
+ @Nullable
protected final List<TaskInfo> mTasks;
/**
@@ -85,6 +92,14 @@
protected final int[] mMinimizedTaskIds;
/**
+ * Only set for TYPE_MIXED.
+ *
+ * The mixed set of task infos in this group.
+ */
+ @Nullable
+ protected final List<GroupedTaskInfo> mGroupedTasks;
+
+ /**
* Create new for a stack of fullscreen tasks
*/
public static GroupedTaskInfo forFullscreenTasks(@NonNull TaskInfo task) {
@@ -111,18 +126,41 @@
minimizedFreeformTasks.stream().mapToInt(i -> i).toArray());
}
+ /**
+ * Create new for a group of grouped task infos, those grouped task infos may not be mixed
+ * themselves (ie. multiple depths of mixed grouped task infos are not allowed).
+ */
+ public static GroupedTaskInfo forMixed(@NonNull List<GroupedTaskInfo> groupedTasks) {
+ if (groupedTasks.isEmpty()) {
+ throw new IllegalArgumentException("Expected non-empty grouped task list");
+ }
+ if (groupedTasks.stream().anyMatch(task -> task.mType == TYPE_MIXED)) {
+ throw new IllegalArgumentException("Unexpected grouped task list");
+ }
+ return new GroupedTaskInfo(groupedTasks);
+ }
+
private GroupedTaskInfo(
@NonNull List<TaskInfo> tasks,
@Nullable SplitBounds splitBounds,
@GroupType int type,
@Nullable int[] minimizedFreeformTaskIds) {
mTasks = tasks;
+ mGroupedTasks = null;
mSplitBounds = splitBounds;
mType = type;
mMinimizedTaskIds = minimizedFreeformTaskIds;
ensureAllMinimizedIdsPresent(tasks, minimizedFreeformTaskIds);
}
+ private GroupedTaskInfo(@NonNull List<GroupedTaskInfo> groupedTasks) {
+ mTasks = null;
+ mGroupedTasks = groupedTasks;
+ mSplitBounds = null;
+ mType = TYPE_MIXED;
+ mMinimizedTaskIds = null;
+ }
+
private void ensureAllMinimizedIdsPresent(
@NonNull List<TaskInfo> tasks,
@Nullable int[] minimizedFreeformTaskIds) {
@@ -141,26 +179,47 @@
for (int i = 0; i < numTasks; i++) {
mTasks.add(new TaskInfo(parcel));
}
+ mGroupedTasks = parcel.createTypedArrayList(GroupedTaskInfo.CREATOR);
mSplitBounds = parcel.readTypedObject(SplitBounds.CREATOR);
mType = parcel.readInt();
mMinimizedTaskIds = parcel.createIntArray();
}
/**
- * Get primary {@link RecentTaskInfo}
+ * If TYPE_MIXED, returns the root of the grouped tasks
+ * For all other types, returns this task itself
+ */
+ @NonNull
+ public GroupedTaskInfo getBaseGroupedTask() {
+ if (mType == TYPE_MIXED) {
+ return mGroupedTasks.getFirst();
+ }
+ return this;
+ }
+
+ /**
+ * Get primary {@link TaskInfo}.
+ *
+ * @throws IllegalStateException if the group is TYPE_MIXED.
*/
@NonNull
public TaskInfo getTaskInfo1() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No indexed tasks for a mixed task");
+ }
return mTasks.getFirst();
}
/**
- * Get secondary {@link RecentTaskInfo}.
+ * Get secondary {@link TaskInfo}, used primarily for TYPE_SPLIT.
*
- * Used in split screen.
+ * @throws IllegalStateException if the group is TYPE_MIXED.
*/
@Nullable
public TaskInfo getTaskInfo2() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No indexed tasks for a mixed task");
+ }
if (mTasks.size() > 1) {
return mTasks.get(1);
}
@@ -172,9 +231,7 @@
*/
@Nullable
public TaskInfo getTaskById(int taskId) {
- return mTasks.stream()
- .filter(task -> task.taskId == taskId)
- .findFirst().orElse(null);
+ return CollectionsKt.firstOrNull(getTaskInfoList(), taskInfo -> taskInfo.taskId == taskId);
}
/**
@@ -182,35 +239,59 @@
*/
@NonNull
public List<TaskInfo> getTaskInfoList() {
- return mTasks;
+ if (mType == TYPE_MIXED) {
+ return CollectionsKt.flatMap(mGroupedTasks, groupedTaskInfo -> groupedTaskInfo.mTasks);
+ } else {
+ return mTasks;
+ }
}
/**
* @return Whether this grouped task contains a task with the given {@code taskId}.
*/
public boolean containsTask(int taskId) {
- return mTasks.stream()
- .anyMatch((task -> task.taskId == taskId));
+ return getTaskById(taskId) != null;
}
/**
- * Return {@link SplitBounds} if this is a split screen entry or {@code null}
+ * Returns whether the group is of the given type, if this is a TYPE_MIXED group, then returns
+ * whether the root task info is of the given type.
+ */
+ public boolean isBaseType(@GroupType int type) {
+ return getBaseGroupedTask().mType == type;
+ }
+
+ /**
+ * Return {@link SplitBounds} if this is a split screen entry or {@code null}. Only valid for
+ * TYPE_SPLIT.
*/
@Nullable
public SplitBounds getSplitBounds() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No split bounds for a mixed task");
+ }
return mSplitBounds;
}
/**
- * Get type of this recents entry. One of {@link GroupType}
+ * Get type of this recents entry. One of {@link GroupType}.
+ * Note: This is deprecated, callers should use `isBaseType()` and not make assumptions about
+ * specific group types
*/
+ @Deprecated
@GroupType
public int getType() {
return mType;
}
+ /**
+ * Returns the set of minimized task ids, only valid for TYPE_FREEFORM.
+ */
@Nullable
public int[] getMinimizedTaskIds() {
+ if (mType == TYPE_MIXED) {
+ throw new IllegalStateException("No minimized task ids for a mixed task");
+ }
return mMinimizedTaskIds;
}
@@ -222,67 +303,64 @@
GroupedTaskInfo other = (GroupedTaskInfo) obj;
return mType == other.mType
&& Objects.equals(mTasks, other.mTasks)
+ && Objects.equals(mGroupedTasks, other.mGroupedTasks)
&& Objects.equals(mSplitBounds, other.mSplitBounds)
&& Arrays.equals(mMinimizedTaskIds, other.mMinimizedTaskIds);
}
@Override
public int hashCode() {
- return Objects.hash(mType, mTasks, mSplitBounds, Arrays.hashCode(mMinimizedTaskIds));
+ return Objects.hash(mType, mTasks, mGroupedTasks, mSplitBounds,
+ Arrays.hashCode(mMinimizedTaskIds));
}
@Override
public String toString() {
StringBuilder taskString = new StringBuilder();
- for (int i = 0; i < mTasks.size(); i++) {
- if (i == 0) {
- taskString.append("Task");
- } else {
- taskString.append(", Task");
+ if (mType == TYPE_MIXED) {
+ taskString.append("GroupedTasks=" + mGroupedTasks.stream()
+ .map(GroupedTaskInfo::toString)
+ .collect(Collectors.joining(",\n\t", "[\n\t", "\n]")));
+ } else {
+ taskString.append("Tasks=" + mTasks.stream()
+ .map(taskInfo -> getTaskInfoDumpString(taskInfo))
+ .collect(Collectors.joining(", ", "[", "]")));
+ if (mSplitBounds != null) {
+ taskString.append(", SplitBounds=").append(mSplitBounds);
}
- taskString.append(i + 1).append(": ").append(getTaskInfo(mTasks.get(i)));
+ taskString.append(", Type=" + typeToString(mType));
+ taskString.append(", Minimized Task IDs=" + Arrays.toString(mMinimizedTaskIds));
}
- if (mSplitBounds != null) {
- taskString.append(", SplitBounds: ").append(mSplitBounds);
- }
- taskString.append(", Type=");
- switch (mType) {
- case TYPE_FULLSCREEN:
- taskString.append("TYPE_FULLSCREEN");
- break;
- case TYPE_SPLIT:
- taskString.append("TYPE_SPLIT");
- break;
- case TYPE_FREEFORM:
- taskString.append("TYPE_FREEFORM");
- break;
- }
- taskString.append(", Minimized Task IDs: ");
- taskString.append(Arrays.toString(mMinimizedTaskIds));
return taskString.toString();
}
- private String getTaskInfo(TaskInfo taskInfo) {
+ private String getTaskInfoDumpString(TaskInfo taskInfo) {
if (taskInfo == null) {
return null;
}
+ final boolean isExcluded = (taskInfo.baseIntent.getFlags()
+ & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0;
return "id=" + taskInfo.taskId
- + " baseIntent=" +
- (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
- ? taskInfo.baseIntent.getComponent().flattenToString()
- : "null")
- + " winMode=" + WindowConfiguration.windowingModeToString(
- taskInfo.getWindowingMode());
+ + " winMode=" + windowingModeToString(taskInfo.getWindowingMode())
+ + " visReq=" + taskInfo.isVisibleRequested
+ + " vis=" + taskInfo.isVisible
+ + " excluded=" + isExcluded
+ + " baseIntent="
+ + (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null
+ ? taskInfo.baseIntent.getComponent().flattenToShortString()
+ : "null");
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
// We don't use the parcel list methods because we want to only write the TaskInfo state
// and not the subclasses (Recents/RunningTaskInfo) whose fields are all deprecated
- parcel.writeInt(mTasks.size());
- for (int i = 0; i < mTasks.size(); i++) {
+ final int tasksSize = mTasks != null ? mTasks.size() : 0;
+ parcel.writeInt(tasksSize);
+ for (int i = 0; i < tasksSize; i++) {
mTasks.get(i).writeTaskToParcel(parcel, flags);
}
+ parcel.writeTypedList(mGroupedTasks);
parcel.writeTypedObject(mSplitBounds, flags);
parcel.writeInt(mType);
parcel.writeIntArray(mMinimizedTaskIds);
@@ -293,6 +371,16 @@
return 0;
}
+ private String typeToString(@GroupType int type) {
+ return switch (type) {
+ case TYPE_FULLSCREEN -> "FULLSCREEN";
+ case TYPE_SPLIT -> "SPLIT";
+ case TYPE_FREEFORM -> "FREEFORM";
+ case TYPE_MIXED -> "MIXED";
+ default -> "UNKNOWN";
+ };
+ }
+
public static final Creator<GroupedTaskInfo> CREATOR = new Creator() {
@Override
public GroupedTaskInfo createFromParcel(Parcel in) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
index 06a55d3..08079d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebUtils.kt
@@ -19,7 +19,6 @@
package com.android.wm.shell.apptoweb
import android.app.assist.AssistContent
-import android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
@@ -113,5 +112,5 @@
* Returns the web uri from the given [AssistContent].
*/
fun AssistContent.getSessionWebUri(): Uri? {
- return extras.getParcelable(EXTRA_SESSION_TRANSFER_WEB_URI) ?: webUri
+ return sessionTransferUri ?: webUri
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index 536dc2a..a4620d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -339,7 +339,7 @@
.setWindowCrop(leash, endBounds.width(), endBounds.height())
.apply()
onTaskResizeAnimationListener?.onAnimationEnd(taskId)
- finishCallback.onTransitionFinished(null /* wct */)
+ finishCallback.onTransitionFinished(/* wct= */ null)
}
)
addUpdateListener { animation ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index ceef699..e8f9a78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -306,7 +306,7 @@
fun logTaskInfoStateInit() {
logTaskUpdate(
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
- /* session_id */ 0,
+ sessionId = 0,
TaskUpdate(
visibleTaskCount = 0,
instanceId = 0,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index cd37113..32ee319 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -274,7 +274,7 @@
lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
final WindowlessWindowManager windowManager = new WindowlessWindowManager(
mTaskInfo.configuration, mLeash,
- null /* hostInputToken */);
+ /* hostInputToken= */ null);
mViewHost = new SurfaceControlViewHost(mContext,
mDisplayController.getDisplay(mTaskInfo.displayId), windowManager,
"DesktopModeVisualIndicator");
@@ -338,7 +338,7 @@
if (mCurrentType == NO_INDICATOR) {
fadeInIndicator(newType);
} else if (newType == NO_INDICATOR) {
- fadeOutIndicator(null /* finishCallback */);
+ fadeOutIndicator(/* finishCallback= */ null);
} else {
final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType(
mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index d180ea7..ee817b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -762,7 +762,7 @@
return
}
val wct = WindowContainerTransaction()
- wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
+ wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true)
startLaunchTransition(
transitionType = TRANSIT_TO_FRONT,
wct = wct,
@@ -884,7 +884,7 @@
} else if (Flags.enableMoveToNextDisplayShortcut()) {
applyFreeformDisplayChange(wct, task, displayId)
}
- wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
+ wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true)
if (Flags.enableDisplayFocusInShellTransitions()) {
// Bring the destination display to top with includingParents=true, so that the
// destination display gains the display focus, which makes the top task in the display
@@ -896,7 +896,7 @@
performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct)
}
- transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
+ transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
/**
@@ -1672,7 +1672,7 @@
requestedTaskId,
splitPosition,
options.toBundle(),
- null, /* hideTaskToken */
+ /* hideTaskToken= */ null,
)
}
}
@@ -1709,8 +1709,8 @@
fillIn,
splitPosition,
options.toBundle(),
- null /* hideTaskToken */,
- true /* forceLaunchNewTask */,
+ /* hideTaskToken= */ null,
+ /* forceLaunchNewTask= */ true,
splitIndex,
)
}
@@ -1961,7 +1961,7 @@
wct.setBounds(taskInfo.token, initialBounds)
}
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
- wct.reorder(taskInfo.token, true /* onTop */)
+ wct.reorder(taskInfo.token, /* onTop= */ true)
if (useDesktopOverrideDensity()) {
wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE)
}
@@ -2796,7 +2796,7 @@
controller,
"visibleTaskCount",
{ controller -> result[0] = controller.visibleTaskCount(displayId) },
- true, /* blocking */
+ /* blocking= */ true,
)
return result[0]
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 0330a5f..c2dd4d28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -234,7 +234,7 @@
// If it's a running task, reorder it to back.
taskIdToMinimize
?.let { shellTaskOrganizer.getRunningTaskInfo(it) }
- ?.let { wct.reorder(it.token, false /* onTop */) }
+ ?.let { wct.reorder(it.token, /* onTop= */ false) }
return taskIdToMinimize
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 72c0642..1380a9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -131,7 +131,7 @@
val pendingIntent =
PendingIntent.getActivityAsUser(
context.createContextAsUser(taskUser, /* flags= */ 0),
- 0 /* requestCode */,
+ /* requestCode= */ 0,
launchHomeIntent,
FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
options.toBundle(),
@@ -234,7 +234,7 @@
val wct = WindowContainerTransaction()
restoreWindowOrder(wct, state)
state.startTransitionFinishTransaction?.apply()
- state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
requestSplitFromScaledTask(splitPosition, wct)
clearState()
} else {
@@ -440,7 +440,7 @@
val wct = WindowContainerTransaction()
restoreWindowOrder(wct)
state.startTransitionFinishTransaction?.apply()
- state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
requestSplitSelect(wct, taskInfo, splitPosition)
}
return true
@@ -492,7 +492,7 @@
finishTransaction = startTransactionFinishT,
)
// Call finishCallback to merge animation before startTransitionFinishCb is called
- finishCallback.onTransitionFinished(null /* wct */)
+ finishCallback.onTransitionFinished(/* wct= */ null)
animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb)
} else if (isCancelTransition) {
info.changes.forEach { change ->
@@ -500,8 +500,8 @@
startTransactionFinishT.show(change.leash)
}
t.apply()
- finishCallback.onTransitionFinished(null /* wct */)
- startTransitionFinishCb.onTransitionFinished(null /* wct */)
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
clearState()
}
}
@@ -653,7 +653,7 @@
interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
} else if (state.cancelTransitionToken == transition) {
state.draggedTaskChange?.leash?.let { state.startTransitionFinishTransaction?.show(it) }
- state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
clearState()
} else {
// This transition being aborted is neither the start, nor the cancel transition, so
@@ -741,19 +741,19 @@
// TODO(b/322852244): investigate why even though these "other" tasks are
// reordered in front of home and behind the translucent dragged task, its
// surface is not visible on screen.
- wct.reorder(wc, true /* toTop */)
+ wct.reorder(wc, /* onTop= */ true)
}
val wc =
state.draggedTaskChange?.container
?: error("Dragged task should be non-null before cancelling")
// Then the dragged task a the very top.
- wct.reorder(wc, true /* toTop */)
+ wct.reorder(wc, /* onTop= */ true)
}
is TransitionState.FromSplit -> {
val wc =
state.splitRootChange?.container
?: error("Split root should be non-null before cancelling")
- wct.reorder(wc, true /* toTop */)
+ wct.reorder(wc, /* onTop= */ true)
}
}
val homeWc =
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
index 964e5fd..af1679f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationController.aidl
@@ -36,12 +36,6 @@
interface IRecentsAnimationController {
/**
- * Takes a screenshot of the task associated with the given {@param taskId}. Only valid for the
- * current set of task ids provided to the handler.
- */
- TaskSnapshot screenshotTask(int taskId);
-
- /**
* Sets the final surface transaction on a Task. This is used by Launcher to notify the system
* that animating Activity to PiP has completed and the associated task surface should be
* updated accordingly. This should be called before `finish`
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 032dac9..76496b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -1227,19 +1227,6 @@
}
@Override
- public TaskSnapshot screenshotTask(int taskId) {
- try {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId);
- return ActivityTaskManager.getService().takeTaskSnapshot(taskId,
- true /* updateCache */);
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to screenshot task", e);
- }
- return null;
- }
-
- @Override
public void setInputConsumerEnabled(boolean enabled) {
mExecutor.execute(() -> {
if (mFinishCB == null || !enabled) {
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 40ecdec..805f4c2 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -67,6 +67,7 @@
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RequiresFlagsDisabled(Flags.FLAG_ENABLE_PIP2)
+@FlakyTest(bugId = 386333280)
open class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
EnterPipTransition(flicker) {
override val pipApp: PipAppHelper = PipAppHelper(instrumentation)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
index db00f41..04f9ada 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
@@ -142,7 +142,7 @@
changeMode: Int = WindowManager.TRANSIT_CLOSE,
task: RunningTaskInfo,
): TransitionInfo =
- TransitionInfo(type, 0 /* flags */).apply {
+ TransitionInfo(type, /* flags= */ 0).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
index d14c640..c705f5a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt
@@ -153,7 +153,7 @@
changeMode: Int = WindowManager.TRANSIT_TO_BACK,
task: RunningTaskInfo,
): TransitionInfo =
- TransitionInfo(type, 0 /* flags */).apply {
+ TransitionInfo(type, /* flags= */ 0).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 3cf84d9..372e47c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -777,7 +777,7 @@
task: RunningTaskInfo,
withWallpaper: Boolean = false,
): TransitionInfo =
- TransitionInfo(WindowManager.TRANSIT_CLOSE, 0 /* flags */).apply {
+ TransitionInfo(WindowManager.TRANSIT_CLOSE, /* flags= */ 0).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index e032616..da27c08 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1267,7 +1267,7 @@
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFullscreenTask().apply {
baseActivity = baseComponent
@@ -1284,7 +1284,7 @@
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFullscreenTask().apply {
baseActivity = baseComponent
@@ -1757,12 +1757,12 @@
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val wallpaperChange =
- hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() }
- assertThat(wallpaperChange).isNotNull()
- assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
- }
+ val wallpaperChange =
+ getLatestWct(type = TRANSIT_CHANGE).hierarchyOps.find { op ->
+ op.container == wallpaperToken.asBinder()
+ }
+ assertNotNull(wallpaperChange)
+ assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
}
@Test
@@ -1792,15 +1792,13 @@
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
- // to the right margin and the ratio of the top margin to bottom margin are also
- // preserved.
- assertThat(taskChange!!.configuration.windowConfiguration.bounds)
- .isEqualTo(Rect(240, 160, 560, 400))
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
+ // to the right margin and the ratio of the top margin to bottom margin are also
+ // preserved.
+ assertThat(taskChange.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(240, 160, 560, 400))
}
@Test
@@ -1831,12 +1829,10 @@
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- assertThat(taskChange!!.configuration.windowConfiguration.bounds)
- .isEqualTo(Rect(960, 480, 1280, 720))
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ assertThat(taskChange.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(960, 480, 1280, 720))
}
@Test
@@ -1864,13 +1860,11 @@
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- // DP size is preserved. The window is centered in the destination display.
- assertThat(taskChange!!.configuration.windowConfiguration.bounds)
- .isEqualTo(Rect(320, 120, 960, 600))
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ // DP size is preserved. The window is centered in the destination display.
+ assertThat(taskChange.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(320, 120, 960, 600))
}
@Test
@@ -1903,14 +1897,12 @@
controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- val taskChange = changes[task.token.asBinder()]
- assertThat(taskChange).isNotNull()
- assertThat(taskChange!!.configuration.windowConfiguration.bounds.left).isAtLeast(0)
- assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
- assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
- assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
- }
+ val taskChange = getLatestWct(type = TRANSIT_CHANGE).changes[task.token.asBinder()]
+ assertNotNull(taskChange)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.left).isAtLeast(0)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
}
@Test
@@ -2722,7 +2714,7 @@
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFreeformTask().apply {
baseActivity = baseComponent
@@ -2743,7 +2735,7 @@
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
- val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
val task =
setUpFullscreenTask().apply {
baseActivity = baseComponent
@@ -3376,11 +3368,11 @@
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, -100), /* position */
- PointF(200f, -200f), /* inputCoordinate */
- Rect(100, -100, 500, 1000), /* currentDragBounds */
- Rect(0, 50, 2000, 2000), /* validDragArea */
- Rect() /* dragStartBounds */,
+ position = Point(100, -100),
+ inputCoordinate = PointF(200f, -200f),
+ currentDragBounds = Rect(100, -100, 500, 1000),
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3415,11 +3407,11 @@
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 200), /* position */
- PointF(200f, 300f), /* inputCoordinate */
- currentDragBounds, /* currentDragBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ position = Point(100, 200),
+ inputCoordinate = PointF(200f, 300f),
+ currentDragBounds = currentDragBounds,
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3459,11 +3451,11 @@
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 50), /* position */
- PointF(200f, 300f), /* inputCoordinate */
- Rect(100, 50, 500, 1000), /* currentDragBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ position = Point(100, 50),
+ inputCoordinate = PointF(200f, 300f),
+ currentDragBounds = Rect(100, 50, 500, 1000),
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3498,11 +3490,11 @@
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 50), /* position */
- PointF(200f, 300f), /* inputCoordinate */
+ position = Point(100, 50),
+ inputCoordinate = PointF(200f, 300f),
currentDragBounds,
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -3555,11 +3547,11 @@
spyController.onDragPositioningEnd(
task,
mockSurface,
- Point(100, 50), /* position */
- PointF(200f, 300f), /* inputCoordinate */
- currentDragBounds, /* currentDragBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */,
- Rect() /* dragStartBounds */,
+ position = Point(100, 50),
+ inputCoordinate = PointF(200f, 300f),
+ currentDragBounds = currentDragBounds,
+ validDragArea = Rect(0, 50, 2000, 2000),
+ dragStartBounds = Rect(),
motionEvent,
desktopWindowDecoration,
)
@@ -5053,7 +5045,7 @@
task: RunningTaskInfo?,
@WindowManager.TransitionType type: Int = TRANSIT_OPEN,
): TransitionRequestInfo {
- return TransitionRequestInfo(type, task, null /* remoteTransition */)
+ return TransitionRequestInfo(type, task, /* remoteTransition= */ null)
}
private companion object {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 52602f2..c8214b3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -193,10 +193,10 @@
desktopTasksLimiter
.getTransitionObserver()
.onTransitionReady(
- Binder() /* transition */,
+ /* transition= */ Binder(),
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -217,10 +217,10 @@
desktopTasksLimiter
.getTransitionObserver()
.onTransitionReady(
- taskTransition /* transition */,
+ /* transition= */ taskTransition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -242,8 +242,8 @@
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
@@ -265,8 +265,8 @@
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -287,8 +287,8 @@
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -316,8 +316,8 @@
.onTransitionReady(
transition,
TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) },
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -344,8 +344,8 @@
.onTransitionReady(
newTransition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
@@ -552,8 +552,8 @@
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
@@ -584,8 +584,8 @@
.onTransitionReady(
transition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition)
@@ -617,8 +617,8 @@
.onTransitionReady(
mergedTransition,
TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
- StubTransaction() /* startTransaction */,
- StubTransaction(), /* finishTransaction */
+ /* startTransaction= */ StubTransaction(),
+ /* finishTransaction= */ StubTransaction(),
)
desktopTasksLimiter.getTransitionObserver().onTransitionStarting(mergedTransition)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
index d491d44..3cc30cb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt
@@ -305,7 +305,7 @@
type: Int = TRANSIT_TO_BACK,
withWallpaper: Boolean = false,
): TransitionInfo {
- return TransitionInfo(type, 0 /* flags */).apply {
+ return TransitionInfo(type, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = type
@@ -331,7 +331,7 @@
task: RunningTaskInfo?,
type: Int = TRANSIT_OPEN,
): TransitionInfo {
- return TransitionInfo(TRANSIT_OPEN, 0 /* flags */).apply {
+ return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_OPEN
@@ -344,7 +344,7 @@
}
private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo {
- return TransitionInfo(TRANSIT_CLOSE, 0 /* flags */).apply {
+ return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_CLOSE
@@ -357,7 +357,7 @@
}
private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo {
- return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply {
+ return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply {
addChange(
Change(mock(), mock()).apply {
mode = TRANSIT_TO_BACK
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 2216d54..341df02 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -677,7 +677,7 @@
}
private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo {
- return TransitionInfo(type, 0 /* flags */).apply {
+ return TransitionInfo(type, /* flags= */ 0).apply {
addChange( // Home.
TransitionInfo.Change(mock(), homeTaskLeash).apply {
parent = null
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index fd3adab..3209664 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -40,6 +40,7 @@
/**
* Tests for [GroupedTaskInfo]
+ * Build & Run: atest WMShellUnitTests:GroupedTaskInfoTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -47,7 +48,7 @@
@Test
fun testSingleTask_hasCorrectType() {
- assertThat(singleTaskGroupInfo().type).isEqualTo(TYPE_FULLSCREEN)
+ assertThat(singleTaskGroupInfo().isBaseType(TYPE_FULLSCREEN)).isTrue()
}
@Test
@@ -66,7 +67,7 @@
@Test
fun testSplitTasks_hasCorrectType() {
- assertThat(splitTasksGroupInfo().type).isEqualTo(TYPE_SPLIT)
+ assertThat(splitTasksGroupInfo().isBaseType(TYPE_SPLIT)).isTrue()
}
@Test
@@ -87,8 +88,8 @@
@Test
fun testFreeformTasks_hasCorrectType() {
- assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).type)
- .isEqualTo(TYPE_FREEFORM)
+ assertThat(freeformTasksGroupInfo(freeformTaskIds = arrayOf(1)).isBaseType(TYPE_FREEFORM))
+ .isTrue()
}
@Test
@@ -111,83 +112,155 @@
}
@Test
+ fun testMixedWithFullscreenBase_hasCorrectType() {
+ assertThat(mixedTaskGroupInfoWithFullscreenBase().isBaseType(TYPE_FULLSCREEN)).isTrue()
+ }
+
+ @Test
+ fun testMixedWithSplitBase_hasCorrectType() {
+ assertThat(mixedTaskGroupInfoWithSplitBase().isBaseType(TYPE_SPLIT)).isTrue()
+ }
+
+ @Test
+ fun testMixedWithFreeformBase_hasCorrectType() {
+ assertThat(mixedTaskGroupInfoWithFreeformBase().isBaseType(TYPE_FREEFORM)).isTrue()
+ }
+
+ @Test
+ fun testMixed_disallowEmptyMixed() {
+ assertThrows(IllegalArgumentException::class.java) {
+ GroupedTaskInfo.forMixed(listOf())
+ }
+ }
+
+ @Test
+ fun testMixed_disallowNestedMixed() {
+ assertThrows(IllegalArgumentException::class.java) {
+ GroupedTaskInfo.forMixed(listOf(
+ GroupedTaskInfo.forMixed(listOf(singleTaskGroupInfo()))))
+ }
+ }
+
+ @Test
+ fun testMixed_disallowNonMixedAccessors() {
+ val mixed = mixedTaskGroupInfoWithFullscreenBase()
+ assertThrows(IllegalStateException::class.java) {
+ mixed.taskInfo1
+ }
+ assertThrows(IllegalStateException::class.java) {
+ mixed.taskInfo2
+ }
+ assertThrows(IllegalStateException::class.java) {
+ mixed.splitBounds
+ }
+ assertThrows(IllegalStateException::class.java) {
+ mixed.minimizedTaskIds
+ }
+ }
+
+ @Test
fun testParcelling_singleTask() {
- val recentTaskInfo = singleTaskGroupInfo()
+ val taskInfo = singleTaskGroupInfo()
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FULLSCREEN)
- assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
- assertThat(recentTaskInfoParcel.taskInfo2).isNull()
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FULLSCREEN)).isTrue()
+ assertThat(taskInfoFromParcel.taskInfo1.taskId).isEqualTo(1)
+ assertThat(taskInfoFromParcel.taskInfo2).isNull()
}
@Test
fun testParcelling_splitTasks() {
- val recentTaskInfo = splitTasksGroupInfo()
+ val taskInfo = splitTasksGroupInfo()
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_SPLIT)
- assertThat(recentTaskInfoParcel.taskInfo1.taskId).isEqualTo(1)
- assertThat(recentTaskInfoParcel.taskInfo2).isNotNull()
- assertThat(recentTaskInfoParcel.taskInfo2!!.taskId).isEqualTo(2)
- assertThat(recentTaskInfoParcel.splitBounds).isNotNull()
- assertThat(recentTaskInfoParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_SPLIT)).isTrue()
+ assertThat(taskInfoFromParcel.taskInfo1.taskId).isEqualTo(1)
+ assertThat(taskInfoFromParcel.taskInfo2).isNotNull()
+ assertThat(taskInfoFromParcel.taskInfo2!!.taskId).isEqualTo(2)
+ assertThat(taskInfoFromParcel.splitBounds).isNotNull()
+ assertThat(taskInfoFromParcel.splitBounds!!.snapPosition).isEqualTo(SNAP_TO_2_50_50)
}
@Test
fun testParcelling_freeformTasks() {
- val recentTaskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3))
+ val taskInfo = freeformTasksGroupInfo(freeformTaskIds = arrayOf(1, 2, 3))
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
- assertThat(recentTaskInfoParcel.taskInfoList).hasSize(3)
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+ assertThat(taskInfoFromParcel.taskInfoList).hasSize(3)
// Only compare task ids
val taskIdComparator = Correspondence.transforming<TaskInfo, Int>(
{ it?.taskId }, "has taskId of"
)
- assertThat(recentTaskInfoParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
- .containsExactly(1, 2, 3)
+ assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
+ .containsExactly(1, 2, 3).inOrder()
}
@Test
fun testParcelling_freeformTasks_minimizedTasks() {
- val recentTaskInfo = freeformTasksGroupInfo(
+ val taskInfo = freeformTasksGroupInfo(
freeformTaskIds = arrayOf(1, 2, 3), minimizedTaskIds = arrayOf(2))
val parcel = Parcel.obtain()
- recentTaskInfo.writeToParcel(parcel, 0)
+ taskInfo.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read the object back from the parcel
- val recentTaskInfoParcel: GroupedTaskInfo =
+ val taskInfoFromParcel: GroupedTaskInfo =
GroupedTaskInfo.CREATOR.createFromParcel(parcel)
- assertThat(recentTaskInfoParcel.type).isEqualTo(TYPE_FREEFORM)
- assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+ assertThat(taskInfoFromParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
}
@Test
- fun testGetTaskById_singleTasks() {
+ fun testParcelling_mixedTasks() {
+ val taskInfo = GroupedTaskInfo.forMixed(listOf(
+ freeformTasksGroupInfo(freeformTaskIds = arrayOf(4, 5, 6),
+ minimizedTaskIds = arrayOf(5)),
+ splitTasksGroupInfo(firstId = 2, secondId = 3),
+ singleTaskGroupInfo(id = 1)))
+
+ val parcel = Parcel.obtain()
+ taskInfo.writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+
+ // Read the object back from the parcel
+ val taskInfoFromParcel: GroupedTaskInfo =
+ GroupedTaskInfo.CREATOR.createFromParcel(parcel)
+ assertThat(taskInfoFromParcel.isBaseType(TYPE_FREEFORM)).isTrue()
+ assertThat(taskInfoFromParcel.baseGroupedTask.minimizedTaskIds).isEqualTo(
+ arrayOf(5).toIntArray())
+ for (i in 1..6) {
+ assertThat(taskInfoFromParcel.containsTask(i)).isTrue()
+ }
+ assertThat(taskInfoFromParcel.taskInfoList).hasSize(taskInfo.taskInfoList.size)
+ }
+
+ @Test
+ fun testTaskProperties_singleTasks() {
val task1 = createTaskInfo(id = 1234)
val taskInfo = GroupedTaskInfo.forFullscreenTasks(task1)
assertThat(taskInfo.getTaskById(1234)).isEqualTo(task1)
assertThat(taskInfo.containsTask(1234)).isTrue()
+ assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1))
}
@Test
- fun testGetTaskById_multipleTasks() {
+ fun testTaskProperties_splitTasks() {
val task1 = createTaskInfo(id = 1)
val task2 = createTaskInfo(id = 2)
val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
@@ -198,6 +271,41 @@
assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
assertThat(taskInfo.containsTask(1)).isTrue()
assertThat(taskInfo.containsTask(2)).isTrue()
+ assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1, task2))
+ }
+
+ @Test
+ fun testTaskProperties_freeformTasks() {
+ val task1 = createTaskInfo(id = 1)
+ val task2 = createTaskInfo(id = 2)
+
+ val taskInfo = GroupedTaskInfo.forFreeformTasks(listOf(task1, task2), setOf())
+
+ assertThat(taskInfo.getTaskById(1)).isEqualTo(task1)
+ assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
+ assertThat(taskInfo.containsTask(1)).isTrue()
+ assertThat(taskInfo.containsTask(2)).isTrue()
+ assertThat(taskInfo.taskInfoList).isEqualTo(listOf(task1, task2))
+ }
+
+ @Test
+ fun testTaskProperties_mixedTasks() {
+ val task1 = createTaskInfo(id = 1)
+ val task2 = createTaskInfo(id = 2)
+ val task3 = createTaskInfo(id = 3)
+ val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
+
+ val splitTasks = GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
+ val fullscreenTasks = GroupedTaskInfo.forFullscreenTasks(task3)
+ val mixedTasks = GroupedTaskInfo.forMixed(listOf(splitTasks, fullscreenTasks))
+
+ assertThat(mixedTasks.getTaskById(1)).isEqualTo(task1)
+ assertThat(mixedTasks.getTaskById(2)).isEqualTo(task2)
+ assertThat(mixedTasks.getTaskById(3)).isEqualTo(task3)
+ assertThat(mixedTasks.containsTask(1)).isTrue()
+ assertThat(mixedTasks.containsTask(2)).isTrue()
+ assertThat(mixedTasks.containsTask(3)).isTrue()
+ assertThat(mixedTasks.taskInfoList).isEqualTo(listOf(task1, task2, task3))
}
private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply {
@@ -205,14 +313,14 @@
token = WindowContainerToken(mock(IWindowContainerToken::class.java))
}
- private fun singleTaskGroupInfo(): GroupedTaskInfo {
- val task = createTaskInfo(id = 1)
+ private fun singleTaskGroupInfo(id: Int = 1): GroupedTaskInfo {
+ val task = createTaskInfo(id)
return GroupedTaskInfo.forFullscreenTasks(task)
}
- private fun splitTasksGroupInfo(): GroupedTaskInfo {
- val task1 = createTaskInfo(id = 1)
- val task2 = createTaskInfo(id = 2)
+ private fun splitTasksGroupInfo(firstId: Int = 1, secondId: Int = 2): GroupedTaskInfo {
+ val task1 = createTaskInfo(firstId)
+ val task2 = createTaskInfo(secondId)
val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
return GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
}
@@ -225,4 +333,22 @@
freeformTaskIds.map { createTaskInfo(it) }.toList(),
minimizedTaskIds.toSet())
}
+
+ private fun mixedTaskGroupInfoWithFullscreenBase(): GroupedTaskInfo {
+ return GroupedTaskInfo.forMixed(listOf(
+ singleTaskGroupInfo(id = 1),
+ singleTaskGroupInfo(id = 2)))
+ }
+
+ private fun mixedTaskGroupInfoWithSplitBase(): GroupedTaskInfo {
+ return GroupedTaskInfo.forMixed(listOf(
+ splitTasksGroupInfo(firstId = 2, secondId = 3),
+ singleTaskGroupInfo(id = 1)))
+ }
+
+ private fun mixedTaskGroupInfoWithFreeformBase(): GroupedTaskInfo {
+ return GroupedTaskInfo.forMixed(listOf(
+ freeformTasksGroupInfo(freeformTaskIds = arrayOf(2, 3, 4)),
+ singleTaskGroupInfo(id = 1)))
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 22b45e8..7e5d6ce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -24,6 +24,9 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.launcher3.Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FULLSCREEN;
+import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static org.junit.Assert.assertEquals;
@@ -346,9 +349,9 @@
GroupedTaskInfo singleGroup2 = recentTasks.get(2);
// Check that groups have expected types
- assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup1.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup2.getType());
+ assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+ assertTrue(singleGroup1.isBaseType(TYPE_FULLSCREEN));
+ assertTrue(singleGroup2.isBaseType(TYPE_FULLSCREEN));
// Check freeform group entries
assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
@@ -385,9 +388,9 @@
GroupedTaskInfo singleGroup = recentTasks.get(2);
// Check that groups have expected types
- assertEquals(GroupedTaskInfo.TYPE_SPLIT, splitGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup.getType());
+ assertTrue(splitGroup.isBaseType(TYPE_SPLIT));
+ assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+ assertTrue(singleGroup.isBaseType(TYPE_FULLSCREEN));
// Check freeform group entries
assertEquals(t3, freeformGroup.getTaskInfoList().get(0));
@@ -420,10 +423,10 @@
// Expect no grouping of tasks
assertEquals(4, recentTasks.size());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(0).getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(1).getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(2).getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, recentTasks.get(3).getType());
+ assertTrue(recentTasks.get(0).isBaseType(TYPE_FULLSCREEN));
+ assertTrue(recentTasks.get(1).isBaseType(TYPE_FULLSCREEN));
+ assertTrue(recentTasks.get(2).isBaseType(TYPE_FULLSCREEN));
+ assertTrue(recentTasks.get(3).isBaseType(TYPE_FULLSCREEN));
assertEquals(t1, recentTasks.get(0).getTaskInfo1());
assertEquals(t2, recentTasks.get(1).getTaskInfo1());
@@ -457,9 +460,9 @@
GroupedTaskInfo singleGroup2 = recentTasks.get(2);
// Check that groups have expected types
- assertEquals(GroupedTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup1.getType());
- assertEquals(GroupedTaskInfo.TYPE_FULLSCREEN, singleGroup2.getType());
+ assertTrue(freeformGroup.isBaseType(TYPE_FREEFORM));
+ assertTrue(singleGroup1.isBaseType(TYPE_FULLSCREEN));
+ assertTrue(singleGroup2.isBaseType(TYPE_FULLSCREEN));
// Check freeform group entries
assertEquals(3, freeformGroup.getTaskInfoList().size());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 7dac085..6b02aef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -20,7 +20,6 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.app.assist.AssistContent.EXTRA_SESSION_TRANSFER_WEB_URI;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
@@ -1176,7 +1175,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
- public void webUriLink_webUriLinkUsedWhenWhenAvailable() {
+ public void sessionTransferUri_sessionTransferUriUsedWhenWhenAvailable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
@@ -1188,7 +1187,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
- public void webUriLink_webUriLinkUsedWhenSessionTransferUriUnavailable() {
+ public void webUri_webUriUsedWhenSessionTransferUriUnavailable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, TEST_URI2 /* web uri */,
@@ -1200,7 +1199,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB)
- public void genericLink_genericLinkUsedWhenCapturedLinkAndWebUriUnavailable() {
+ public void genericLink_genericLinkUsedWhenCapturedLinkAndAssistContentUriUnavailable() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */);
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, null /* captured link */, null /* web uri */,
@@ -1490,7 +1489,7 @@
taskInfo.capturedLink = capturedLink;
taskInfo.capturedLinkTimestamp = System.currentTimeMillis();
mAssistContent.setWebUri(webUri);
- mAssistContent.getExtras().putObject(EXTRA_SESSION_TRANSFER_WEB_URI, sessionTransferUri);
+ mAssistContent.setSessionTransferUri(sessionTransferUri);
final String genericLinkString = genericLink == null ? null : genericLink.toString();
doReturn(genericLinkString).when(mMockGenericLinksParser).getGenericLink(any());
// Relayout to set captured link
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 36f62da..c9625c4 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -5866,19 +5866,31 @@
@NonNull MediaCodec codec, @NonNull MediaFormat format);
/**
- * Called when the metrics for this codec have been flushed due to the
- * start of a new subsession.
+ * Called when the metrics for this codec have been flushed "mid-stream"
+ * due to the start of a new subsession during execution.
* <p>
- * This can happen when the codec is reconfigured after stop(), or
- * mid-stream e.g. if the video size changes. When this happens, the
- * metrics for the previous subsession are flushed, and
- * {@link MediaCodec#getMetrics} will return the metrics for the
- * new subsession. This happens just before the {@link Callback#onOutputFormatChanged}
+ * A new codec subsession normally starts when the codec is reconfigured
+ * after stop(), but it can also happen mid-stream e.g. if the video size
+ * changes. When this happens, the metrics for the previous subsession
+ * are flushed, and {@link MediaCodec#getMetrics} will return the metrics
+ * for the new subsession.
+ * <p>
+ * For subsessions that begin due to a reconfiguration, the metrics for
+ * the prior subsession can be retrieved via {@link MediaCodec#getMetrics}
+ * prior to calling {@link #configure}.
+ * <p>
+ * When a new subsession begins "mid-stream", the metrics for the prior
+ * subsession are flushed just before the {@link Callback#onOutputFormatChanged}
* event, so this <b>optional</b> callback is provided to be able to
* capture the final metrics for the previous subsession.
*
* @param codec The MediaCodec object.
- * @param metrics The flushed metrics for this codec.
+ * @param metrics The flushed metrics for this codec. This is a
+ * {@link PersistableBundle} containing the set of
+ * attributes and values available for the media being
+ * handled by this instance of MediaCodec. The attributes
+ * are described in {@link MetricsConstants}. Additional
+ * vendor-specific fields may also be present.
*/
@FlaggedApi(FLAG_SUBSESSION_METRICS)
public void onMetricsFlushed(
diff --git a/media/java/android/media/MediaMuxer.java b/media/java/android/media/MediaMuxer.java
index 678150b..4c5efc1 100644
--- a/media/java/android/media/MediaMuxer.java
+++ b/media/java/android/media/MediaMuxer.java
@@ -18,6 +18,8 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.media.MediaCodec.BufferInfo;
import android.os.Build;
@@ -257,6 +259,8 @@
*/
private OutputFormat() {}
/** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
public static final int MUXER_OUTPUT_FIRST = 0;
/** MPEG4 media file format*/
public static final int MUXER_OUTPUT_MPEG_4 = MUXER_OUTPUT_FIRST;
@@ -269,6 +273,8 @@
/** Ogg media file format*/
public static final int MUXER_OUTPUT_OGG = MUXER_OUTPUT_FIRST + 4;
/** @hide */
+ @SuppressLint("UnflaggedApi")
+ @TestApi
public static final int MUXER_OUTPUT_LAST = MUXER_OUTPUT_OGG;
};
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index f629c88..b30b779 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -320,6 +320,9 @@
ASystemFontIterator_open; # introduced=29
ASystemFontIterator_close; # introduced=29
ASystemFontIterator_next; # introduced=29
+ ASystemHealth_getMaxCpuHeadroomTidsSize; # introduced=36
+ ASystemHealth_getCpuHeadroomCalculationWindowRange; # introduced=36
+ ASystemHealth_getGpuHeadroomCalculationWindowRange; # introduced=36
ASystemHealth_getCpuHeadroom; # introduced=36
ASystemHealth_getGpuHeadroom; # introduced=36
ASystemHealth_getCpuHeadroomMinIntervalMillis; # introduced=36
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index 68c1983..1e6a7b7 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -859,7 +859,7 @@
std::vector<ANativeWindow*> windowVec(windows, windows + numWindows);
for (auto&& window : windowVec) {
Surface* surface = static_cast<Surface*>(window);
- if (Surface::isValid(surface)) {
+ if (surface != nullptr) {
const sp<IBinder>& handle = surface->getSurfaceControlHandle();
if (handle != nullptr) {
out.push_back(handle);
diff --git a/native/android/system_health.cpp b/native/android/system_health.cpp
index f3fa9f6..5c07ac7 100644
--- a/native/android/system_health.cpp
+++ b/native/android/system_health.cpp
@@ -31,26 +31,28 @@
struct ACpuHeadroomParams : public CpuHeadroomParamsInternal {};
struct AGpuHeadroomParams : public GpuHeadroomParamsInternal {};
-const int CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
-const int CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
-const int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN = 50;
-const int GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX = 10000;
-const int CPU_HEADROOM_MAX_TID_COUNT = 5;
-
struct ASystemHealthManager {
public:
static ASystemHealthManager* getInstance();
- ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager);
+
+ ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager,
+ IHintManager::HintManagerClientData&& clientData);
ASystemHealthManager() = delete;
~ASystemHealthManager();
int getCpuHeadroom(const ACpuHeadroomParams* params, float* outHeadroom);
int getGpuHeadroom(const AGpuHeadroomParams* params, float* outHeadroom);
int getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis);
int getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis);
+ int getMaxCpuHeadroomTidsSize(size_t* outSize);
+ int getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis);
+ int getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis);
private:
static ASystemHealthManager* create(std::shared_ptr<IHintManager> hintManager);
std::shared_ptr<IHintManager> mHintManager;
+ IHintManager::HintManagerClientData mClientData;
};
ASystemHealthManager* ASystemHealthManager::getInstance() {
@@ -60,10 +62,11 @@
return instance;
}
-ASystemHealthManager::ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager)
- : mHintManager(std::move(hintManager)) {}
+ASystemHealthManager::ASystemHealthManager(std::shared_ptr<IHintManager>& hintManager,
+ IHintManager::HintManagerClientData&& clientData)
+ : mHintManager(std::move(hintManager)), mClientData(clientData) {}
-ASystemHealthManager::~ASystemHealthManager() {}
+ASystemHealthManager::~ASystemHealthManager() = default;
ASystemHealthManager* ASystemHealthManager::create(std::shared_ptr<IHintManager> hintManager) {
if (!hintManager) {
@@ -74,20 +77,37 @@
ALOGE("%s: PerformanceHint service is not ready ", __FUNCTION__);
return nullptr;
}
- return new ASystemHealthManager(hintManager);
-}
-
-ASystemHealthManager* ASystemHealth_acquireManager() {
- return ASystemHealthManager::getInstance();
+ IHintManager::HintManagerClientData clientData;
+ ndk::ScopedAStatus ret = hintManager->getClientData(&clientData);
+ if (!ret.isOk()) {
+ ALOGE("%s: PerformanceHint service is not initialized %s", __FUNCTION__, ret.getMessage());
+ return nullptr;
+ }
+ return new ASystemHealthManager(hintManager, std::move(clientData));
}
int ASystemHealthManager::getCpuHeadroom(const ACpuHeadroomParams* params, float* outHeadroom) {
+ if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
std::optional<hal::CpuHeadroomResult> res;
::ndk::ScopedAStatus ret;
CpuHeadroomParamsInternal internalParams;
if (!params) {
ret = mHintManager->getCpuHeadroom(internalParams, &res);
} else {
+ LOG_ALWAYS_FATAL_IF((int)params->tids.size() > mClientData.maxCpuHeadroomThreads,
+ "%s: tids size should not exceed %d", __FUNCTION__,
+ mClientData.maxCpuHeadroomThreads);
+ LOG_ALWAYS_FATAL_IF(params->calculationWindowMillis <
+ mClientData.supportInfo.headroom
+ .cpuMinCalculationWindowMillis ||
+ params->calculationWindowMillis >
+ mClientData.supportInfo.headroom
+ .cpuMaxCalculationWindowMillis,
+ "%s: calculationWindowMillis should be in range [%d, %d] but got %d",
+ __FUNCTION__,
+ mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis,
+ mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis,
+ params->calculationWindowMillis);
ret = mHintManager->getCpuHeadroom(*params, &res);
}
if (!ret.isOk()) {
@@ -106,12 +126,24 @@
}
int ASystemHealthManager::getGpuHeadroom(const AGpuHeadroomParams* params, float* outHeadroom) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
std::optional<hal::GpuHeadroomResult> res;
::ndk::ScopedAStatus ret;
GpuHeadroomParamsInternal internalParams;
if (!params) {
ret = mHintManager->getGpuHeadroom(internalParams, &res);
} else {
+ LOG_ALWAYS_FATAL_IF(params->calculationWindowMillis <
+ mClientData.supportInfo.headroom
+ .gpuMinCalculationWindowMillis ||
+ params->calculationWindowMillis >
+ mClientData.supportInfo.headroom
+ .gpuMaxCalculationWindowMillis,
+ "%s: calculationWindowMillis should be in range [%d, %d] but got %d",
+ __FUNCTION__,
+ mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis,
+ mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis,
+ params->calculationWindowMillis);
ret = mHintManager->getGpuHeadroom(*params, &res);
}
if (!ret.isOk()) {
@@ -128,6 +160,7 @@
}
int ASystemHealthManager::getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) {
+ if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
int64_t minIntervalMillis = 0;
::ndk::ScopedAStatus ret = mHintManager->getCpuHeadroomMinIntervalMillis(&minIntervalMillis);
if (!ret.isOk()) {
@@ -142,6 +175,7 @@
}
int ASystemHealthManager::getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
int64_t minIntervalMillis = 0;
::ndk::ScopedAStatus ret = mHintManager->getGpuHeadroomMinIntervalMillis(&minIntervalMillis);
if (!ret.isOk()) {
@@ -155,6 +189,57 @@
return OK;
}
+int ASystemHealthManager::getMaxCpuHeadroomTidsSize(size_t* outSize) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
+ *outSize = mClientData.maxCpuHeadroomThreads;
+ return OK;
+}
+
+int ASystemHealthManager::getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP;
+ *outMinMillis = mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis;
+ *outMaxMillis = mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis;
+ return OK;
+}
+
+int ASystemHealthManager::getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP;
+ *outMinMillis = mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis;
+ *outMaxMillis = mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis;
+ return OK;
+}
+
+int ASystemHealth_getMaxCpuHeadroomTidsSize(size_t* _Nonnull outSize) {
+ LOG_ALWAYS_FATAL_IF(outSize == nullptr, "%s: outSize should not be null", __FUNCTION__);
+ auto manager = ASystemHealthManager::getInstance();
+ if (manager == nullptr) return ENOTSUP;
+ return manager->getMaxCpuHeadroomTidsSize(outSize);
+}
+
+int ASystemHealth_getCpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ LOG_ALWAYS_FATAL_IF(outMinMillis == nullptr, "%s: outMinMillis should not be null",
+ __FUNCTION__);
+ LOG_ALWAYS_FATAL_IF(outMaxMillis == nullptr, "%s: outMaxMillis should not be null",
+ __FUNCTION__);
+ auto manager = ASystemHealthManager::getInstance();
+ if (manager == nullptr) return ENOTSUP;
+ return manager->getCpuHeadroomCalculationWindowRange(outMinMillis, outMaxMillis);
+}
+
+int ASystemHealth_getGpuHeadroomCalculationWindowRange(int32_t* _Nonnull outMinMillis,
+ int32_t* _Nonnull outMaxMillis) {
+ LOG_ALWAYS_FATAL_IF(outMinMillis == nullptr, "%s: outMinMillis should not be null",
+ __FUNCTION__);
+ LOG_ALWAYS_FATAL_IF(outMaxMillis == nullptr, "%s: outMaxMillis should not be null",
+ __FUNCTION__);
+ auto manager = ASystemHealthManager::getInstance();
+ if (manager == nullptr) return ENOTSUP;
+ return manager->getGpuHeadroomCalculationWindowRange(outMinMillis, outMaxMillis);
+}
+
int ASystemHealth_getCpuHeadroom(const ACpuHeadroomParams* _Nullable params,
float* _Nonnull outHeadroom) {
LOG_ALWAYS_FATAL_IF(outHeadroom == nullptr, "%s: outHeadroom should not be null", __FUNCTION__);
@@ -189,19 +274,15 @@
void ACpuHeadroomParams_setCalculationWindowMillis(ACpuHeadroomParams* _Nonnull params,
int windowMillis) {
- LOG_ALWAYS_FATAL_IF(windowMillis < CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN ||
- windowMillis > CPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX,
- "%s: windowMillis should be in range [50, 10000] but got %d", __FUNCTION__,
- windowMillis);
+ LOG_ALWAYS_FATAL_IF(windowMillis <= 0, "%s: windowMillis should be positive but got %d",
+ __FUNCTION__, windowMillis);
params->calculationWindowMillis = windowMillis;
}
void AGpuHeadroomParams_setCalculationWindowMillis(AGpuHeadroomParams* _Nonnull params,
int windowMillis) {
- LOG_ALWAYS_FATAL_IF(windowMillis < GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MIN ||
- windowMillis > GPU_HEADROOM_CALCULATION_WINDOW_MILLIS_MAX,
- "%s: windowMillis should be in range [50, 10000] but got %d", __FUNCTION__,
- windowMillis);
+ LOG_ALWAYS_FATAL_IF(windowMillis <= 0, "%s: windowMillis should be positive but got %d",
+ __FUNCTION__, windowMillis);
params->calculationWindowMillis = windowMillis;
}
@@ -214,13 +295,11 @@
}
void ACpuHeadroomParams_setTids(ACpuHeadroomParams* _Nonnull params, const int* _Nonnull tids,
- int tidsSize) {
+ size_t tidsSize) {
LOG_ALWAYS_FATAL_IF(tids == nullptr, "%s: tids should not be null", __FUNCTION__);
- LOG_ALWAYS_FATAL_IF(tidsSize > CPU_HEADROOM_MAX_TID_COUNT, "%s: tids size should not exceed 5",
- __FUNCTION__);
params->tids.resize(tidsSize);
params->tids.clear();
- for (int i = 0; i < tidsSize; ++i) {
+ for (int i = 0; i < (int)tidsSize; ++i) {
LOG_ALWAYS_FATAL_IF(tids[i] <= 0, "ACpuHeadroomParams_setTids: Invalid non-positive tid %d",
tids[i]);
params->tids[i] = tids[i];
@@ -269,10 +348,10 @@
return new AGpuHeadroomParams();
}
-void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nonnull params) {
+void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nullable params) {
delete params;
}
-void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nonnull params) {
+void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nullable params) {
delete params;
}
diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
index f68fa1a..0fa92ea 100644
--- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
+++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp
@@ -161,6 +161,9 @@
clientDataIn,
::aidl::android::os::IHintManager::HintManagerClientData* _aidl_return),
(override));
+ MOCK_METHOD(ScopedAStatus, getClientData,
+ (::aidl::android::os::IHintManager::HintManagerClientData * _aidl_return),
+ (override));
MOCK_METHOD(SpAIBinder, asBinder, (), (override));
MOCK_METHOD(bool, isRemote, (), (override));
};
@@ -602,6 +605,15 @@
ASSERT_NE(config, nullptr);
}
+TEST_F(PerformanceHintTest, TestSessionCreationWithNullLayers) {
+ EXPECT_CALL(*mMockIHintManager, createHintSessionWithConfig(_, _, _, _, _)).Times(1);
+ auto&& config = configFromCreator(
+ {.tids = mTids, .nativeWindows = {nullptr}, .surfaceControls = {nullptr}});
+ APerformanceHintManager* manager = createManager();
+ auto&& session = createSessionUsingConfig(manager, config);
+ ASSERT_TRUE(session);
+}
+
TEST_F(PerformanceHintTest, TestSupportObject) {
// Disable GPU and Power Efficiency support to test partial enabling
mClientData.supportInfo.sessionModes &= ~(1 << (int)hal::SessionMode::AUTO_GPU);
diff --git a/packages/CredentialManager/tests/robotests/Android.bp b/packages/CredentialManager/tests/robotests/Android.bp
index 27afaaa..01f403d 100644
--- a/packages/CredentialManager/tests/robotests/Android.bp
+++ b/packages/CredentialManager/tests/robotests/Android.bp
@@ -53,7 +53,6 @@
"android.test.mock.stubs.system",
"truth",
],
- upstream: true,
java_resource_dirs: ["config"],
instrumentation_for: "CredentialManagerRobo",
}
diff --git a/packages/CredentialManager/wear/robotests/Android.bp b/packages/CredentialManager/wear/robotests/Android.bp
index 589a3d6..db3c363 100644
--- a/packages/CredentialManager/wear/robotests/Android.bp
+++ b/packages/CredentialManager/wear/robotests/Android.bp
@@ -24,6 +24,5 @@
"framework_graphics_flags_java_lib",
],
java_resource_dirs: ["config"],
- upstream: true,
strict_mode: false,
}
diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
new file mode 100644
index 0000000..b384a24
--- /dev/null
+++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm
@@ -0,0 +1,357 @@
+# 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.
+
+#
+# Romanian keyboard layout.
+#
+
+type OVERLAY
+
+map key 86 PLUS
+
+### ROW 1
+
+key GRAVE {
+ label: '\u201e'
+ base: '\u201e'
+ shift: '\u201d'
+ ralt: '`'
+ ralt+shift: '~'
+}
+
+key 1 {
+ label: '1'
+ base: '1'
+ shift: '!'
+ ralt: '\u0303'
+}
+
+key 2 {
+ label: '2'
+ base: '2'
+ shift: '@'
+ ralt: '\u030C'
+}
+
+key 3 {
+ label: '3'
+ base: '3'
+ shift: '#'
+ ralt: '\u0302'
+}
+
+key 4 {
+ label: '4'
+ base: '4'
+ shift: '$'
+ ralt: '\u0306'
+}
+
+key 5 {
+ label: '5'
+ base: '5'
+ shift: '%'
+ ralt: '\u030A'
+}
+
+key 6 {
+ label: '6'
+ base: '6'
+ shift: '^'
+ ralt: '\u0328'
+}
+
+key 7 {
+ label: '7'
+ base: '7'
+ shift: '&'
+ ralt: '\u0300'
+}
+
+key 8 {
+ label: '8'
+ base: '8'
+ shift: '*'
+ ralt: '\u0307'
+}
+
+key 9 {
+ label: '9'
+ base: '9'
+ shift: '('
+ ralt: '\u0301'
+}
+
+key 0 {
+ label: '0'
+ base: '0'
+ shift: ')'
+ ralt: '\u030B'
+}
+
+key MINUS {
+ label: '-'
+ base: '-'
+ shift: '_'
+ ralt: '\u0308'
+ ralt+shift: '\u2013'
+}
+
+key EQUALS {
+ label: '='
+ base: '='
+ shift: '+'
+ ralt: '\u0327'
+ ralt+shift: '\u00b1'
+}
+
+### ROW 2
+
+key Q {
+ label: 'Q'
+ base, capslock+shift: 'q'
+ shift, capslock: 'Q'
+}
+
+key W {
+ label: 'W'
+ base, capslock+shift: 'w'
+ shift, capslock: 'W'
+}
+
+key E {
+ label: 'E'
+ base, capslock+shift: 'e'
+ shift, capslock: 'E'
+ ralt: '\u20ac'
+}
+
+key R {
+ label: 'R'
+ base, capslock+shift: 'r'
+ shift, capslock: 'R'
+}
+
+key T {
+ label: 'T'
+ base, capslock+shift: 't'
+ shift, capslock: 'T'
+}
+
+key Y {
+ label: 'Y'
+ base, capslock+shift: 'y'
+ shift, capslock: 'Y'
+}
+
+key U {
+ label: 'U'
+ base, capslock+shift: 'u'
+ shift, capslock: 'U'
+}
+
+key I {
+ label: 'I'
+ base, capslock+shift: 'i'
+ shift, capslock: 'I'
+}
+
+key O {
+ label: 'O'
+ base, capslock+shift: 'o'
+ shift, capslock: 'O'
+}
+
+key P {
+ label: 'P'
+ base, capslock+shift: 'p'
+ shift, capslock: 'P'
+ ralt: '\u00a7'
+}
+
+key LEFT_BRACKET {
+ label: '\u0102'
+ base, capslock+shift: '\u0103'
+ shift, capslock: '\u0102'
+ ralt: '['
+ ralt+shift: '{'
+}
+
+key RIGHT_BRACKET {
+ label: '\u00ce'
+ base, capslock+shift: '\u00ee'
+ shift, capslock: '\u00ce'
+ ralt: ']'
+ ralt+shift: '}'
+}
+
+### ROW 3
+
+key A {
+ label: 'A'
+ base, capslock+shift: 'a'
+ shift, capslock: 'A'
+}
+
+key S {
+ label: 'S'
+ base, capslock+shift: 's'
+ shift, capslock: 'S'
+ ralt: '\u00df'
+}
+
+key D {
+ label: 'D'
+ base, capslock+shift: 'd'
+ shift, capslock: 'D'
+ ralt: '\u0111'
+ ralt+shift, ralt+capslock: '\u0110'
+ ralt+shift+capslock: '\u0111'
+}
+
+key F {
+ label: 'F'
+ base, capslock+shift: 'f'
+ shift, capslock: 'F'
+}
+
+key G {
+ label: 'G'
+ base, capslock+shift: 'g'
+ shift, capslock: 'G'
+}
+
+key H {
+ label: 'H'
+ base, capslock+shift: 'h'
+ shift, capslock: 'H'
+}
+
+key J {
+ label: 'J'
+ base, capslock+shift: 'j'
+ shift, capslock: 'J'
+}
+
+key K {
+ label: 'K'
+ base, capslock+shift: 'k'
+ shift, capslock: 'K'
+}
+
+key L {
+ label: 'L'
+ base, capslock+shift: 'l'
+ shift, capslock: 'L'
+ ralt: '\u0142'
+ ralt+shift, ralt+capslock: '\u0141'
+ ralt+shift+capslock: '\u0142'
+}
+
+key SEMICOLON {
+ label: '\u0218'
+ base, capslock+shift: '\u0219'
+ shift, capslock: '\u0218'
+ ralt: ';'
+ ralt+shift: ':'
+}
+
+key APOSTROPHE {
+ label: '\u021a'
+ base, capslock+shift: '\u021b'
+ shift, capslock: '\u021a'
+ ralt: '\''
+ ralt+shift: '\u0022'
+}
+
+key BACKSLASH {
+ label: '\u00c2'
+ base, capslock+shift: '\u00e2'
+ shift, capslock: '\u00c2'
+ ralt: '\\'
+ ralt+shift: '|'
+}
+
+### ROW 4
+
+key PLUS {
+ label: '\\'
+ base: '\\'
+ shift: '|'
+}
+
+key Z {
+ label: 'Z'
+ base, capslock+shift: 'z'
+ shift, capslock: 'Z'
+}
+
+key X {
+ label: 'X'
+ base, capslock+shift: 'x'
+ shift, capslock: 'X'
+}
+
+key C {
+ label: 'C'
+ base, capslock+shift: 'c'
+ shift, capslock: 'C'
+ ralt: '\u00a9'
+}
+
+key V {
+ label: 'V'
+ base, capslock+shift: 'v'
+ shift, capslock: 'V'
+}
+
+key B {
+ label: 'B'
+ base, capslock+shift: 'b'
+ shift, capslock: 'B'
+}
+
+key N {
+ label: 'N'
+ base, capslock+shift: 'n'
+ shift, capslock: 'N'
+}
+
+key M {
+ label: 'M'
+ base, capslock+shift: 'm'
+ shift, capslock: 'M'
+}
+
+key COMMA {
+ label: ','
+ base: ','
+ shift: ';'
+ ralt: '<'
+ ralt+shift: '\u00ab'
+}
+
+key PERIOD {
+ label: '.'
+ base: '.'
+ shift: ':'
+ ralt: '>'
+ ralt+shift: '\u00bb'
+}
+
+key SLASH {
+ label: '/'
+ base: '/'
+ shift: '?'
+}
diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml
index 5a91125..bd7cdc4 100644
--- a/packages/InputDevices/res/values/strings.xml
+++ b/packages/InputDevices/res/values/strings.xml
@@ -164,4 +164,7 @@
<!-- Montenegrin (Cyrillic) keyboard layout label. [CHAR LIMIT=35] -->
<string name="keyboard_layout_montenegrin_cyrillic">Montenegrin (Cyrillic)</string>
+
+ <!-- Romanian keyboard layout label. [CHAR LIMIT=35] -->
+ <string name="keyboard_layout_romanian">Romanian</string>
</resources>
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 9309489..9ce9a87 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -360,4 +360,11 @@
android:keyboardLayout="@raw/keyboard_layout_serbian_and_montenegrin_cyrillic"
android:keyboardLocale="cnr-Cyrl-ME"
android:keyboardLayoutType="extended" />
+
+ <keyboard-layout
+ android:name="keyboard_layout_romanian"
+ android:label="@string/keyboard_layout_romanian"
+ android:keyboardLayout="@raw/keyboard_layout_romanian"
+ android:keyboardLocale="ro-Latn-RO"
+ android:keyboardLayoutType="qwerty" />
</keyboard-layouts>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
index b20117d..c99d37b 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ARCHIVED_PACKAGES;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import android.app.Activity;
import android.app.DialogFragment;
@@ -53,6 +54,8 @@
@Override
public void onCreate(Bundle icicle) {
+ getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+
super.onCreate(null);
int callingUid = getLaunchedFromUid();
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
index 42dd382..fbb0fa4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveFragment.java
@@ -21,10 +21,14 @@
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.widget.Button;
public class UnarchiveFragment extends DialogFragment implements
DialogInterface.OnClickListener {
+ private Dialog mDialog;
+ private Button mRestoreButton;
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
String appTitle = getArguments().getString(UnarchiveActivity.APP_TITLE);
@@ -40,7 +44,32 @@
dialogBuilder.setPositiveButton(R.string.restore, this);
dialogBuilder.setNegativeButton(android.R.string.cancel, this);
- return dialogBuilder.create();
+ mDialog = dialogBuilder.create();
+ return mDialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mDialog != null) {
+ mRestoreButton = ((AlertDialog) mDialog).getButton(DialogInterface.BUTTON_POSITIVE);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mRestoreButton != null) {
+ mRestoreButton.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mRestoreButton != null) {
+ mRestoreButton.setEnabled(true);
+ }
}
@Override
diff --git a/packages/SettingsLib/DataStore/tests/Android.bp b/packages/SettingsLib/DataStore/tests/Android.bp
index 2e3b42d..6044eab 100644
--- a/packages/SettingsLib/DataStore/tests/Android.bp
+++ b/packages/SettingsLib/DataStore/tests/Android.bp
@@ -25,6 +25,5 @@
java_resource_dirs: ["config"],
instrumentation_for: "SettingsLibDataStoreShell",
coverage_libs: ["SettingsLibDataStore"],
- upstream: true,
strict_mode: false,
}
diff --git a/packages/SettingsLib/Ipc/Android.bp b/packages/SettingsLib/Ipc/Android.bp
index 2c7209a..bc5a936 100644
--- a/packages/SettingsLib/Ipc/Android.bp
+++ b/packages/SettingsLib/Ipc/Android.bp
@@ -25,7 +25,7 @@
name: "SettingsLibIpc-testutils",
srcs: ["testutils/**/*.kt"],
static_libs: [
- "Robolectric_all-target_upstream",
+ "Robolectric_all-target",
"SettingsLibIpc",
"androidx.test.core",
"flag-junit",
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
index f6477e2..dd6743b 100644
--- a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
+++ b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
@@ -68,7 +68,6 @@
"android.test.mock.stubs.system",
"truth",
],
- upstream: true,
java_resource_dirs: ["config"],
instrumentation_for: "SpaRoboApp",
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
new file mode 100644
index 0000000..5b7e2a8
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.icu.text.DecimalFormat
+import android.icu.text.MeasureFormat
+import android.icu.text.NumberFormat
+import android.icu.text.UnicodeSet
+import android.icu.text.UnicodeSetSpanner
+import android.icu.util.Measure
+import android.text.format.Formatter
+import android.text.format.Formatter.RoundedBytesResult
+import java.math.BigDecimal
+
+class BytesFormatter(resources: Resources) {
+
+ enum class UseCase(val flag: Int) {
+ FileSize(Formatter.FLAG_SI_UNITS),
+ DataUsage(Formatter.FLAG_IEC_UNITS),
+ }
+
+ data class Result(val number: String, val units: String)
+
+ constructor(context: Context) : this(context.resources)
+
+ private val locale = resources.configuration.locales[0]
+
+ fun format(bytes: Long, useCase: UseCase): String {
+ val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
+ val numberFormatter = getNumberFormatter(rounded.fractionDigits)
+ return numberFormatter.formatRoundedBytesResult(rounded)
+ }
+
+ fun formatWithUnits(bytes: Long, useCase: UseCase): Result {
+ val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag)
+ val numberFormatter = getNumberFormatter(rounded.fractionDigits)
+ val formattedString = numberFormatter.formatRoundedBytesResult(rounded)
+ val formattedNumber = numberFormatter.format(rounded.value)
+ return Result(
+ number = formattedNumber,
+ units = formattedString.removeFirst(formattedNumber),
+ )
+ }
+
+ private fun NumberFormat.formatRoundedBytesResult(rounded: RoundedBytesResult): String {
+ val measureFormatter =
+ MeasureFormat.getInstance(locale, MeasureFormat.FormatWidth.SHORT, this)
+ return measureFormatter.format(Measure(rounded.value, rounded.units))
+ }
+
+ private fun getNumberFormatter(fractionDigits: Int) =
+ NumberFormat.getInstance(locale).apply {
+ minimumFractionDigits = fractionDigits
+ maximumFractionDigits = fractionDigits
+ isGroupingUsed = false
+ if (this is DecimalFormat) {
+ setRoundingMode(BigDecimal.ROUND_HALF_UP)
+ }
+ }
+
+ private companion object {
+ fun String.removeFirst(removed: String): String =
+ SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString()
+
+ val SPACES_AND_CONTROLS = UnicodeSetSpanner(UnicodeSet("[[:Zs:][:Cf:]]").freeze())
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt
new file mode 100644
index 0000000..7220848
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatterTest.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.framework.common
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BytesFormatterTest {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private val formatter = BytesFormatter(context)
+
+ @Test
+ fun `Zero bytes`() {
+ // Given a byte value of 0, the formatted output should be "0 byte" for both FileSize
+ // and DataUsage UseCases. This verifies special handling of zero values.
+
+ val fileSizeResult = formatter.format(0, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("0 byte")
+
+ val dataUsageResult = formatter.format(0, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("0 byte")
+ }
+
+ @Test
+ fun `Positive bytes`() {
+ // Given a positive byte value (e.g., 1000), the formatted output should be correctly
+ // displayed with appropriate units (e.g., '1.00 kB') for both UseCases.
+
+ val fileSizeResult = formatter.format(1000, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("1.00 kB")
+
+ val dataUsageResult = formatter.format(1024, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("1.00 kB")
+ }
+
+ @Test
+ fun `Large bytes`() {
+ // Given a very large byte value (e.g., Long.MAX_VALUE), the formatted output should be
+ // correctly displayed with the largest unit (e.g., 'PB') for both UseCases.
+
+ val fileSizeResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("9223 PB")
+
+ val dataUsageResult = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("8192 PB")
+ }
+
+ @Test
+ fun `Bytes requiring rounding`() {
+ // Given byte values that require rounding (e.g., 1512), the formatted output should be
+ // rounded to the appropriate number of decimal places (e.g., '1.51 kB').
+
+ val fileSizeResult = formatter.format(1512, BytesFormatter.UseCase.FileSize)
+ assertThat(fileSizeResult).isEqualTo("1.51 kB")
+
+ val dataUsageResult = formatter.format(1512, BytesFormatter.UseCase.DataUsage)
+ assertThat(dataUsageResult).isEqualTo("1.48 kB")
+ }
+
+ @Test
+ fun `FileSize UseCase`() {
+ // When the UseCase is FileSize, the correct units (byte, KB, kB, GB, TB, PB) should
+ // be used.
+ val values =
+ listOf(
+ 1L,
+ 1024L,
+ 1024L * 1024L,
+ 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L * 1024L,
+ )
+ val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB")
+
+ values.zip(expectedUnits).forEach { (value, expectedUnit) ->
+ val result = formatter.format(value, BytesFormatter.UseCase.FileSize)
+ assertThat(result).contains(expectedUnit)
+ }
+ }
+
+ @Test
+ fun `DataUsage UseCase`() {
+ // When the UseCase is DataUsage, the correct units (byte, kB, MB, GB, TB, PB) should
+ // be used.
+ val values =
+ listOf(
+ 1L,
+ 1024L,
+ 1024L * 1024L,
+ 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L,
+ 1024L * 1024L * 1024L * 1024L * 1024L * 1024L,
+ )
+ val expectedUnits = listOf("byte", "kB", "MB", "GB", "TB", "PB", "PB")
+
+ values.zip(expectedUnits).forEach { (value, expectedUnit) ->
+ val result = formatter.format(value, BytesFormatter.UseCase.DataUsage)
+ assertThat(result).contains(expectedUnit)
+ }
+ }
+
+ @Test
+ fun `Fraction digits`() {
+ // The number of fraction digits in the output should be correctly determined based on
+ // the rounded byte value.
+
+ assertThat(formatter.format(1500, BytesFormatter.UseCase.FileSize)).isEqualTo("1.50 kB")
+ assertThat(formatter.format(1050, BytesFormatter.UseCase.FileSize)).isEqualTo("1.05 kB")
+ assertThat(formatter.format(999, BytesFormatter.UseCase.FileSize)).isEqualTo("1.00 kB")
+ }
+
+ @Test
+ fun `Rounding mode`() {
+ // The rounding mode used for formatting should be ROUND_HALF_UP.
+
+ val result = formatter.format(1006, BytesFormatter.UseCase.FileSize)
+
+ assertThat(result).isEqualTo("1.01 kB") // Ensure rounding mode is effective
+ }
+
+ @Test
+ fun `Grouping separator`() {
+ // Grouping separators should not be used in the formatted output.
+
+ val result = formatter.format(Long.MAX_VALUE, BytesFormatter.UseCase.FileSize)
+
+ assertThat(result).isEqualTo("9223 PB")
+ }
+
+ @Test
+ fun `Format with units`() {
+ // Verify that the `formatWithUnits` method correctly formats the given bytes with the
+ // specified units.
+
+ val resultByte = formatter.formatWithUnits(0, BytesFormatter.UseCase.FileSize)
+ assertThat(resultByte).isEqualTo(BytesFormatter.Result("0", "byte"))
+
+ val resultKb = formatter.formatWithUnits(1000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultKb).isEqualTo(BytesFormatter.Result("1.00", "kB"))
+
+ val resultMb = formatter.formatWithUnits(479_999_999, BytesFormatter.UseCase.FileSize)
+ assertThat(resultMb).isEqualTo(BytesFormatter.Result("480", "MB"))
+
+ val resultGb = formatter.formatWithUnits(20_100_000_000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultGb).isEqualTo(BytesFormatter.Result("20.10", "GB"))
+
+ val resultTb =
+ formatter.formatWithUnits(300_100_000_000_000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultTb).isEqualTo(BytesFormatter.Result("300", "TB"))
+
+ val resultPb =
+ formatter.formatWithUnits(1000_000_000_000_000, BytesFormatter.UseCase.FileSize)
+ assertThat(resultPb).isEqualTo(BytesFormatter.Result("1.00", "PB"))
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 145b62c..68e9fe7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -73,6 +73,10 @@
private static final Set<Integer> SA_PROFILES =
ImmutableSet.of(
BluetoothProfile.A2DP, BluetoothProfile.LE_AUDIO, BluetoothProfile.HEARING_AID);
+ private static final List<Integer> BLUETOOTH_DEVICE_CLASS_HEADSET =
+ List.of(
+ BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES,
+ BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET);
private static final String TEMP_BOND_TYPE = "TEMP_BOND_TYPE";
private static final String TEMP_BOND_DEVICE_METADATA_VALUE = "le_audio_sharing";
@@ -390,6 +394,19 @@
return false;
}
+ /** Checks whether the bluetooth device is a headset. */
+ public static boolean isHeadset(@NonNull BluetoothDevice bluetoothDevice) {
+ String deviceType =
+ BluetoothUtils.getStringMetaData(
+ bluetoothDevice, BluetoothDevice.METADATA_DEVICE_TYPE);
+ if (!TextUtils.isEmpty(deviceType)) {
+ return BluetoothDevice.DEVICE_TYPE_HEADSET.equals(deviceType)
+ || BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.equals(deviceType);
+ }
+ BluetoothClass btClass = bluetoothDevice.getBluetoothClass();
+ return btClass != null && BLUETOOTH_DEVICE_CLASS_HEADSET.contains(btClass.getDeviceClass());
+ }
+
/** Create an Icon pointing to a drawable. */
public static IconCompat createIconWithDrawable(Drawable drawable) {
Bitmap bitmap;
diff --git a/packages/SettingsLib/tests/robotests/Android.bp b/packages/SettingsLib/tests/robotests/Android.bp
index 81358ca..117ca85 100644
--- a/packages/SettingsLib/tests/robotests/Android.bp
+++ b/packages/SettingsLib/tests/robotests/Android.bp
@@ -65,7 +65,6 @@
test_options: {
timeout: 36000,
},
- upstream: true,
strict_mode: false,
}
@@ -100,10 +99,10 @@
plugins: [
"auto_value_plugin_1.9",
"auto_value_builder_plugin_1.9",
- "Robolectric_processor_upstream",
+ "Robolectric_processor",
],
libs: [
- "Robolectric_all-target_upstream",
+ "Robolectric_all-target",
"mockito-robolectric-prebuilt",
"truth",
],
diff --git a/packages/SettingsLib/tests/robotests/fragment/Android.bp b/packages/SettingsLib/tests/robotests/fragment/Android.bp
index 3e67156..0214874 100644
--- a/packages/SettingsLib/tests/robotests/fragment/Android.bp
+++ b/packages/SettingsLib/tests/robotests/fragment/Android.bp
@@ -28,13 +28,13 @@
//"-J-verbose",
],
libs: [
- "Robolectric_all-target_upstream",
+ "Robolectric_all-target",
"androidx.fragment_fragment",
],
plugins: [
"auto_value_plugin_1.9",
"auto_value_builder_plugin_1.9",
- "Robolectric_processor_upstream",
+ "Robolectric_processor",
],
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index d49447f..cafe19f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -80,7 +80,9 @@
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private CachedBluetoothDevice mCachedBluetoothDevice;
- @Mock private BluetoothDevice mBluetoothDevice;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private BluetoothDevice mBluetoothDevice;
+
@Mock private AudioManager mAudioManager;
@Mock private PackageManager mPackageManager;
@Mock private LeAudioProfile mA2dpProfile;
@@ -399,6 +401,38 @@
}
@Test
+ public void isHeadset_metadataMatched_returnTrue() {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE))
+ .thenReturn(BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.getBytes());
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isHeadset_metadataNotMatched_returnFalse() {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_DEVICE_TYPE))
+ .thenReturn(BluetoothDevice.DEVICE_TYPE_CARKIT.getBytes());
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isHeadset_btClassMatched_returnTrue() {
+ when(mBluetoothDevice.getBluetoothClass().getDeviceClass())
+ .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES);
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isHeadset_btClassNotMatched_returnFalse() {
+ when(mBluetoothDevice.getBluetoothClass().getDeviceClass())
+ .thenReturn(BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER);
+
+ assertThat(BluetoothUtils.isHeadset(mBluetoothDevice)).isFalse();
+ }
+
+ @Test
public void isAvailableMediaBluetoothDevice_isConnectedLeAudioDevice_returnTrue() {
when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index b9f8c71..1fc1f05 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -186,11 +186,6 @@
Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT,
Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
Settings.Secure.NAVIGATION_MODE,
- Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED,
- Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED,
Settings.Secure.SKIP_GESTURE_COUNT,
Settings.Secure.SKIP_TOUCH_COUNT,
Settings.Secure.SILENCE_ALARMS_GESTURE_COUNT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 7c5e577..d0e88d5 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -284,11 +284,6 @@
new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_RIGHT,
new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_BACK_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_HOME_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.AWARE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SKIP_GESTURE_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.SKIP_TOUCH_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index b88ae37..227fff5 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -85,6 +85,9 @@
filegroup {
name: "SystemUI-tests-broken-robofiles-run",
srcs: [
+ "tests/src/**/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt",
+ "tests/src/**/systemui/power/PowerNotificationWarningsTest.java",
+ "tests/src/**/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt",
"tests/src/**/systemui/dreams/touch/CommunalTouchHandlerTest.java",
"tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt",
"tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt",
@@ -841,7 +844,6 @@
"androidx.test.ext.truth",
],
- upstream: true,
instrumentation_for: "SystemUIRobo-stub",
java_resource_dirs: ["tests/robolectric/config"],
@@ -879,7 +881,6 @@
"androidx.test.ext.truth",
],
- upstream: true,
instrumentation_for: "SystemUIRobo-stub",
java_resource_dirs: ["tests/robolectric/config"],
@@ -916,6 +917,7 @@
"android.test.mock.impl",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
plugins: [
"dagger2-compiler",
],
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 99d704f..51ea529 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -413,7 +413,8 @@
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
tools:replace="android:appComponentFactory"
- android:appComponentFactory=".PhoneSystemUIAppComponentFactory">
+ android:appComponentFactory=".PhoneSystemUIAppComponentFactory"
+ android:enableOnBackInvokedCallback="true">
<!-- Keep theme in sync with SystemUIApplication.onCreate().
Setting the theme on the application does not affect views inflated by services.
The application theme is set again from onCreate to take effect for those views. -->
diff --git a/packages/SystemUI/aconfig/predictive_back.aconfig b/packages/SystemUI/aconfig/predictive_back.aconfig
index 46eb9e1..ee918c2 100644
--- a/packages/SystemUI/aconfig/predictive_back.aconfig
+++ b/packages/SystemUI/aconfig/predictive_back.aconfig
@@ -2,29 +2,8 @@
container: "system"
flag {
- name: "predictive_back_sysui"
- namespace: "systemui"
- description: "Predictive Back Dispatching for SysUI"
- bug: "327737297"
-}
-
-flag {
name: "predictive_back_animate_shade"
namespace: "systemui"
description: "Enable Shade Animations"
bug: "327732946"
}
-
-flag {
- name: "predictive_back_animate_bouncer"
- namespace: "systemui"
- description: "Enable Predictive Back Animation in Bouncer"
- bug: "327733487"
-}
-
-flag {
- name: "predictive_back_animate_dialogs"
- namespace: "systemui"
- description: "Enable Predictive Back Animation for SysUI dialogs"
- bug: "327721544"
-}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
index 91fad4f..56d85ab 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java
@@ -107,7 +107,7 @@
IBinder mergeTarget,
IRemoteTransitionFinishedCallback finishCallback) {
logD("mergeAnimation - " + info);
- mHandler.post(this::cancel);
+ cancel();
}
@Override
@@ -129,7 +129,7 @@
@Override
public void onTransitionConsumed(IBinder transition, boolean aborted) {
logD("onTransitionConsumed - aborted: " + aborted);
- mHandler.post(this::cancel);
+ cancel();
}
private void startAnimationInternal(
@@ -342,11 +342,14 @@
mFinishCallback = null;
}
- private void cancel() {
+ public void cancel() {
logD("cancel()");
- if (mAnimator != null) {
- mAnimator.cancel();
- }
+ mHandler.post(
+ () -> {
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ });
}
private static void logD(String msg) {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
index 6d6aa88..cb3dfb9 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -43,6 +43,7 @@
/**
* A session object that holds origin transition states for starting an activity from an on-screen
* UI component and smoothly transitioning back from the activity to the same UI component.
+ *
* @hide
*/
public class OriginTransitionSession {
@@ -143,6 +144,12 @@
logE("Unable to cancel origin transition!", e);
}
}
+ if (mEntryTransition instanceof OriginRemoteTransition) {
+ ((OriginRemoteTransition) mEntryTransition).cancel();
+ }
+ if (mExitTransition instanceof OriginRemoteTransition) {
+ ((OriginRemoteTransition) mExitTransition).cancel();
+ }
}
private boolean hasEntryTransition() {
@@ -182,6 +189,7 @@
/**
* A builder to build a {@link OriginTransitionSession}.
+ *
* @hide
*/
public static class Builder {
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
index 7c219c6..2e8f928 100644
--- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java
@@ -24,13 +24,15 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver.OnDrawListener;
+import android.view.ViewTreeObserver;
import java.util.ArrayList;
import java.util.List;
@@ -51,8 +53,9 @@
private final Path mClippingPath = new Path();
private final Outline mClippingOutline = new Outline();
- private final OnDrawListener mOnDrawListener = this::postDraw;
+ private final LifecycleListener mLifecycleListener = new LifecycleListener();
private final View mView;
+ private final Handler mMainHandler;
@Nullable private SurfaceControl mSurfaceControl;
@Nullable private Surface mSurface;
@@ -62,6 +65,7 @@
public ViewUIComponent(View view) {
mView = view;
+ mMainHandler = new Handler(Looper.getMainLooper());
}
/**
@@ -110,11 +114,11 @@
t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
// Make sure view draw triggers surface draw.
- mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
+ mLifecycleListener.register();
// Make the view invisible AFTER the surface is shown.
t.addTransactionCommittedListener(
- mView::post,
+ this::post,
() -> {
logD("Surface attached!");
forceDraw();
@@ -129,14 +133,14 @@
SurfaceControl sc = mSurfaceControl;
mSurface = null;
mSurfaceControl = null;
- mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
+ mLifecycleListener.unregister();
// Restore view visibility
mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
// Clean up surfaces.
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(sc, null)
.addTransactionCommittedListener(
- mView::post,
+ this::post,
() -> {
s.release();
sc.release();
@@ -269,7 +273,66 @@
return;
}
mDirty = true;
- mView.post(this::draw);
+ post(this::draw);
+ }
+
+ private void post(Runnable r) {
+ if (mView.isAttachedToWindow()) {
+ mView.post(r);
+ } else {
+ // If the view is detached from window, {@code View.post()} will postpone the action
+ // until the view is attached again. However, we don't know if the view will be attached
+ // again, so we post the action to the main thread in this case. This could lead to race
+ // condition if the attachment change caused a thread switching, and it's the caller's
+ // responsibility to ensure the window attachment state doesn't change unexpectedly.
+ if (DEBUG) {
+ Log.w(TAG, mView + " is not attached. Posting action to main thread!");
+ }
+ mMainHandler.post(r);
+ }
+ }
+
+ /** A listener for monitoring view life cycles. */
+ private class LifecycleListener
+ implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener {
+ private boolean mRegistered;
+
+ @Override
+ public void onDraw() {
+ // View draw should trigger surface draw.
+ postDraw();
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ // empty
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ Log.w(
+ TAG,
+ v + " is detached from the window. Unregistering the life cycle listener ...");
+ unregister();
+ }
+
+ public void register() {
+ if (mRegistered) {
+ return;
+ }
+ mRegistered = true;
+ mView.getViewTreeObserver().addOnDrawListener(this);
+ mView.addOnAttachStateChangeListener(this);
+ }
+
+ public void unregister() {
+ if (!mRegistered) {
+ return;
+ }
+ mRegistered = false;
+ mView.getViewTreeObserver().removeOnDrawListener(this);
+ mView.removeOnAttachStateChangeListener(this);
+ }
}
/** @hide */
@@ -278,34 +341,33 @@
@Override
public Transaction setAlpha(ViewUIComponent ui, float alpha) {
- mChanges.add(() -> ui.mView.post(() -> ui.setAlpha(alpha)));
+ mChanges.add(() -> ui.post(() -> ui.setAlpha(alpha)));
return this;
}
@Override
public Transaction setVisible(ViewUIComponent ui, boolean visible) {
- mChanges.add(() -> ui.mView.post(() -> ui.setVisible(visible)));
+ mChanges.add(() -> ui.post(() -> ui.setVisible(visible)));
return this;
}
@Override
public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
- mChanges.add(() -> ui.mView.post(() -> ui.setBounds(bounds)));
+ mChanges.add(() -> ui.post(() -> ui.setBounds(bounds)));
return this;
}
@Override
public Transaction attachToTransitionLeash(
ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
- mChanges.add(
- () -> ui.mView.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
+ mChanges.add(() -> ui.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
return this;
}
@Override
public Transaction detachFromTransitionLeash(
ViewUIComponent ui, Executor executor, Runnable onDone) {
- mChanges.add(() -> ui.mView.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
+ mChanges.add(() -> ui.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
return this;
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
deleted file mode 100644
index 1c9dabb..0000000
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.android.systemui.animation
-
-interface AnimationFeatureFlags {
- val isPredictiveBackQsDialogAnim: Boolean
- get() = false
-}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index 907c39d..c88c4ebb 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -59,13 +59,8 @@
private val mainExecutor: Executor,
private val callback: Callback,
private val interactionJankMonitor: InteractionJankMonitor,
- private val featureFlags: AnimationFeatureFlags,
private val transitionAnimator: TransitionAnimator =
- TransitionAnimator(
- mainExecutor,
- TIMINGS,
- INTERPOLATORS,
- ),
+ TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS),
private val isForTesting: Boolean = false,
) {
private companion object {
@@ -219,7 +214,7 @@
dialog: Dialog,
view: View,
cuj: DialogCuj? = null,
- animateBackgroundBoundsChange: Boolean = false
+ animateBackgroundBoundsChange: Boolean = false,
) {
val controller = Controller.fromView(view, cuj)
if (controller == null) {
@@ -245,7 +240,7 @@
fun show(
dialog: Dialog,
controller: Controller,
- animateBackgroundBoundsChange: Boolean = false
+ animateBackgroundBoundsChange: Boolean = false,
) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw IllegalStateException(
@@ -263,15 +258,14 @@
val controller =
animatedParent?.dialogContentWithBackground?.let {
Controller.fromView(it, controller.cuj)
- }
- ?: controller
+ } ?: controller
// Make sure we don't run the launch animation from the same source twice at the same time.
if (openedDialogs.any { it.controller.sourceIdentity == controller.sourceIdentity }) {
Log.e(
TAG,
"Not running dialog launch animation from source as it is already expanded into a" +
- " dialog"
+ " dialog",
)
dialog.show()
return
@@ -288,7 +282,6 @@
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
parentAnimatedDialog = animatedParent,
forceDisableSynchronization = isForTesting,
- featureFlags = featureFlags,
)
openedDialogs.add(animatedDialog)
@@ -305,7 +298,7 @@
dialog: Dialog,
animateFrom: Dialog,
cuj: DialogCuj? = null,
- animateBackgroundBoundsChange: Boolean = false
+ animateBackgroundBoundsChange: Boolean = false,
) {
val view =
openedDialogs.firstOrNull { it.dialog == animateFrom }?.dialogContentWithBackground
@@ -313,7 +306,7 @@
Log.w(
TAG,
"Showing dialog $dialog normally as the dialog it is shown from was not shown " +
- "using DialogTransitionAnimator"
+ "using DialogTransitionAnimator",
)
dialog.show()
return
@@ -323,7 +316,7 @@
dialog,
view,
animateBackgroundBoundsChange = animateBackgroundBoundsChange,
- cuj = cuj
+ cuj = cuj,
)
}
@@ -346,8 +339,7 @@
val animatedDialog =
openedDialogs.firstOrNull {
it.dialog.window?.decorView?.viewRootImpl == view.viewRootImpl
- }
- ?: return null
+ } ?: return null
return createActivityTransitionController(animatedDialog, cujType)
}
@@ -373,7 +365,7 @@
private fun createActivityTransitionController(
animatedDialog: AnimatedDialog,
- cujType: Int? = null
+ cujType: Int? = null,
): ActivityTransitionAnimator.Controller? {
// At this point, we know that the intent of the caller is to dismiss the dialog to show
// an app, so we disable the exit animation into the source because we will never want to
@@ -440,7 +432,7 @@
}
private fun disableDialogDismiss() {
- dialog.setDismissOverride { /* Do nothing */}
+ dialog.setDismissOverride { /* Do nothing */ }
}
private fun enableDialogDismiss() {
@@ -530,7 +522,6 @@
* Whether synchronization should be disabled, which can be useful if we are running in a test.
*/
private val forceDisableSynchronization: Boolean,
- private val featureFlags: AnimationFeatureFlags,
) {
/**
* The DecorView of this dialog window.
@@ -643,8 +634,7 @@
originalDialogBackgroundColor =
GhostedViewTransitionAnimatorController.findGradientDrawable(background)
?.color
- ?.defaultColor
- ?: Color.BLACK
+ ?.defaultColor ?: Color.BLACK
// Make the background view invisible until we start the animation. We use the transition
// visibility like GhostView does so that we don't mess up with the accessibility tree (see
@@ -700,7 +690,7 @@
oldLeft: Int,
oldTop: Int,
oldRight: Int,
- oldBottom: Int
+ oldBottom: Int,
) {
dialogContentWithBackground.removeOnLayoutChangeListener(this)
@@ -717,9 +707,7 @@
// the dialog.
dialog.setDismissOverride(this::onDialogDismissed)
- if (featureFlags.isPredictiveBackQsDialogAnim) {
- dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
- }
+ dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
// Show the dialog.
dialog.show()
@@ -815,7 +803,7 @@
if (hasInstrumentedJank) {
interactionJankMonitor.end(controller.cuj!!.cujType)
}
- }
+ },
)
}
@@ -888,14 +876,14 @@
onAnimationFinished(true /* instantDismiss */)
onDialogDismissed(this@AnimatedDialog)
}
- }
+ },
)
}
private fun startAnimation(
isLaunching: Boolean,
onLaunchAnimationStart: () -> Unit = {},
- onLaunchAnimationEnd: () -> Unit = {}
+ onLaunchAnimationEnd: () -> Unit = {},
) {
// Create 2 controllers to animate both the dialog and the source.
val startController =
@@ -969,7 +957,7 @@
override fun onTransitionAnimationProgress(
state: TransitionAnimator.State,
progress: Float,
- linearProgress: Float
+ linearProgress: Float,
) {
startController.onTransitionAnimationProgress(state, progress, linearProgress)
@@ -1026,7 +1014,7 @@
oldLeft: Int,
oldTop: Int,
oldRight: Int,
- oldBottom: Int
+ oldBottom: Int,
) {
// Don't animate if bounds didn't actually change.
if (left == oldLeft && top == oldTop && right == oldRight && bottom == oldBottom) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 46e0efa..183929c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -43,6 +43,7 @@
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.overscroll
@@ -180,10 +181,12 @@
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
) {
+
val isHeadsUp by viewModel.isHeadsUpOrAnimatingAway.collectAsStateWithLifecycle(false)
var scrollOffset by remember { mutableFloatStateOf(0f) }
- val minScrollOffset = -(stackScrollView.getHeadsUpInset().toFloat())
+ val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
+ val minScrollOffset = -headsUpInset
val maxScrollOffset = 0f
val scrollableState = rememberScrollableState { delta ->
@@ -241,6 +244,12 @@
)
}
+/** Y position of the HUNs at rest, when the shade is closed. */
+@Composable
+fun headsUpTopInset(): Dp =
+ WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding() +
+ dimensionResource(R.dimen.heads_up_status_bar_padding)
+
/** Adds the space where notification stack should appear in the scene. */
@Composable
fun ContentScope.ConstrainedNotificationStack(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 75226b3..2e1100a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -19,6 +19,7 @@
import android.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -31,7 +32,6 @@
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -45,6 +45,7 @@
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -60,7 +61,11 @@
* the Activity/Fragment/View hosting this Composable once a result is available.
*/
@Composable
-fun PeopleScreen(viewModel: PeopleViewModel, onResult: (PeopleViewModel.Result) -> Unit) {
+fun PeopleScreen(
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+ modifier: Modifier = Modifier,
+) {
val priorityTiles by viewModel.priorityTiles.collectAsStateWithLifecycle()
val recentTiles by viewModel.recentTiles.collectAsStateWithLifecycle()
@@ -74,7 +79,7 @@
}
}
- Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
+ Surface(color = MaterialTheme.colorScheme.background, modifier = modifier.fillMaxSize()) {
if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel.onTileClicked)
} else {
@@ -88,9 +93,10 @@
priorityTiles: List<PeopleTileViewModel>,
recentTiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
+ modifier: Modifier = Modifier,
) {
Column(
- Modifier.fillMaxSize().safeDrawingPadding().sysuiResTag("top_level_with_conversations")
+ modifier.fillMaxSize().safeDrawingPadding().sysuiResTag("top_level_with_conversations")
) {
Column(
Modifier.fillMaxWidth().padding(PeopleSpacePadding),
@@ -139,28 +145,32 @@
@StringRes headerTextResource: Int,
tiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
+ modifier: Modifier = Modifier,
) {
- Text(
- stringResource(headerTextResource),
- Modifier.padding(start = 16.dp),
- style = MaterialTheme.typography.labelLarge,
- color = MaterialTheme.colorScheme.primary,
- )
+ val largeCornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
+ val smallCornerRadius = 4.dp
- Spacer(Modifier.height(10.dp))
+ fun topRadius(i: Int): Dp = if (i == 0) largeCornerRadius else smallCornerRadius
+ fun bottomRadius(i: Int): Dp =
+ if (i == tiles.lastIndex) largeCornerRadius else smallCornerRadius
- tiles.forEachIndexed { index, tile ->
- if (index > 0) {
- HorizontalDivider(color = MaterialTheme.colorScheme.background, thickness = 2.dp)
- }
+ Column(modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ Text(
+ stringResource(headerTextResource),
+ Modifier.padding(start = 16.dp, bottom = 8.dp),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ )
- key(tile.key.toString()) {
- Tile(
- tile,
- onTileClicked,
- withTopCornerRadius = index == 0,
- withBottomCornerRadius = index == tiles.lastIndex,
- )
+ tiles.forEachIndexed { index, tile ->
+ key(tile.key.toString()) {
+ Tile(
+ tile,
+ onTileClicked,
+ topCornerRadius = topRadius(index),
+ bottomCornerRadius = bottomRadius(index),
+ )
+ }
}
}
}
@@ -169,14 +179,12 @@
private fun Tile(
tile: PeopleTileViewModel,
onTileClicked: (PeopleTileViewModel) -> Unit,
- withTopCornerRadius: Boolean,
- withBottomCornerRadius: Boolean,
+ topCornerRadius: Dp,
+ bottomCornerRadius: Dp,
+ modifier: Modifier = Modifier,
) {
- val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
- val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
- val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
-
Surface(
+ modifier,
color = MaterialTheme.colorScheme.secondaryContainer,
shape =
RoundedCornerShape(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
index 527314d..d4dea65 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -44,9 +44,9 @@
import com.android.systemui.res.R
@Composable
-internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit) {
+internal fun PeopleScreenEmpty(onGotItClicked: () -> Unit, modifier: Modifier = Modifier) {
Column(
- Modifier.fillMaxSize().safeDrawingPadding().padding(PeopleSpacePadding),
+ modifier.fillMaxSize().safeDrawingPadding().padding(PeopleSpacePadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
@@ -74,8 +74,9 @@
}
@Composable
-private fun ExampleTile() {
+private fun ExampleTile(modifier: Modifier = Modifier) {
Surface(
+ modifier,
shape = RoundedCornerShape(28.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index e4f4df3..9ee25c3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -24,6 +24,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
@@ -34,6 +35,7 @@
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace
+import com.android.systemui.notifications.ui.composable.headsUpTopInset
import com.android.systemui.qs.ui.composable.QuickSettings
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaLandscapeTopOffset
import com.android.systemui.qs.ui.composable.QuickSettings.SharedValues.MediaOffset.Default
@@ -78,6 +80,8 @@
}
}
+ val headsUpInset = with(LocalDensity.current) { headsUpTopInset().toPx() }
+
LaunchedEffect(isIdleAndNotOccluded) {
// Wait for being Idle on this Scene, otherwise LaunchedEffect would fire too soon,
// and another transition could override the NSSL stack bounds.
@@ -86,7 +90,7 @@
// and not to confuse the StackScrollAlgorithm when it displays a HUN over GONE.
notificationStackScrolLView.get().apply {
// use -headsUpInset to allow HUN translation outside bounds for snoozing
- setStackTop(-getHeadsUpInset().toFloat())
+ setStackTop(-headsUpInset)
setStackCutoff(0f)
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 568a358..8153586 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -232,6 +232,8 @@
canShowOverlay: (OverlayKey) -> Boolean = { true },
canHideOverlay: (OverlayKey) -> Boolean = { true },
canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
+ onTransitionStart: (TransitionState.Transition) -> Unit = {},
+ onTransitionEnd: (TransitionState.Transition) -> Unit = {},
): MutableSceneTransitionLayoutState {
return MutableSceneTransitionLayoutStateImpl(
initialScene,
@@ -241,6 +243,8 @@
canShowOverlay,
canHideOverlay,
canReplaceOverlay,
+ onTransitionStart,
+ onTransitionEnd,
)
}
@@ -252,7 +256,11 @@
internal val canChangeScene: (SceneKey) -> Boolean = { true },
internal val canShowOverlay: (OverlayKey) -> Boolean = { true },
internal val canHideOverlay: (OverlayKey) -> Boolean = { true },
- internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
+ internal val canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ ->
+ true
+ },
+ private val onTransitionStart: (TransitionState.Transition) -> Unit = {},
+ private val onTransitionEnd: (TransitionState.Transition) -> Unit = {},
) : MutableSceneTransitionLayoutState {
private val creationThread: Thread = Thread.currentThread()
@@ -367,9 +375,11 @@
startTransitionInternal(transition, chain)
// Run the transition until it is finished.
+ onTransitionStart(transition)
transition.runInternal()
} finally {
finishTransition(transition)
+ onTransitionEnd(transition)
}
}
@@ -384,14 +394,10 @@
val toContent = transition.toContent
// Update the transition specs.
- transition.transformationSpec =
- transitions
- .transitionSpec(fromContent, toContent, key = transition.key)
- .transformationSpec(transition)
- transition.previewTransformationSpec =
- transitions
- .transitionSpec(fromContent, toContent, key = transition.key)
- .previewTransformationSpec(transition)
+ val spec = transitions.transitionSpec(fromContent, toContent, key = transition.key)
+ transition._cuj = spec.cuj
+ transition.transformationSpec = spec.transformationSpec(transition)
+ transition.previewTransformationSpec = spec.previewTransformationSpec(transition)
}
private fun startTransitionInternal(transition: TransitionState.Transition, chain: Boolean) {
@@ -411,9 +417,7 @@
if (tooManyTransitions) logTooManyTransitions()
// Force finish all transitions.
- while (currentTransitions.isNotEmpty()) {
- finishTransition(transitionStates[0] as TransitionState.Transition)
- }
+ currentTransitions.fastForEach { finishTransition(it) }
// We finished all transitions, so we are now idle. We remove this state so that
// we end up only with the new transition after appending it.
@@ -475,46 +479,36 @@
// Mark this transition as finished.
finishedTransitions.add(transition)
- // Keep a reference to the last transition, in case we remove all transitions and should
- // settle to Idle.
+ if (finishedTransitions.size != transitionStates.size) {
+ // Some transitions were not finished, so we won't settle to idle.
+ return
+ }
+
+ // Keep a reference to the last transition, in case all transitions are finished and we
+ // should settle to Idle.
val lastTransition = transitionStates.last()
- // Remove all first n finished transitions.
- var i = 0
- val nStates = transitionStates.size
- while (i < nStates) {
- val t = transitionStates[i]
- if (!finishedTransitions.contains(t)) {
- // Stop here.
- break
+ transitionStates.fastForEach { state ->
+ if (!finishedTransitions.contains(state)) {
+ // Some transitions were not finished, so we won't settle to idle.
+ return
}
-
- // Remove the transition from the set of finished transitions.
- finishedTransitions.remove(t)
- i++
}
- // If all transitions are finished, we are idle.
- if (i == nStates) {
- check(finishedTransitions.isEmpty())
- val idle =
- TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
- Log.i(TAG, "all transitions finished. idle=$idle")
- this.transitionStates = listOf(idle)
- } else if (i > 0) {
- this.transitionStates = transitionStates.subList(fromIndex = i, toIndex = nStates)
- }
+ val idle = TransitionState.Idle(lastTransition.currentScene, lastTransition.currentOverlays)
+ Log.i(TAG, "all transitions finished. idle=$idle")
+ finishedTransitions.clear()
+ this.transitionStates = listOf(idle)
}
override fun snapToScene(scene: SceneKey, currentOverlays: Set<OverlayKey>) {
checkThread()
// Force finish all transitions.
- while (currentTransitions.isNotEmpty()) {
- finishTransition(transitionStates[0] as TransitionState.Transition)
- }
+ currentTransitions.fastForEach { finishTransition(it) }
check(transitionStates.size == 1)
+ check(currentTransitions.isEmpty())
transitionStates = listOf(TransitionState.Idle(scene, currentOverlays))
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 756d71c..ff8efc2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -29,6 +29,7 @@
import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.TransformationMatcher
import com.android.compose.animation.scene.transformation.TransformationWithRange
+import com.android.internal.jank.Cuj.CujType
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions
@@ -111,7 +112,15 @@
}
private fun defaultTransition(from: ContentKey, to: ContentKey) =
- TransitionSpecImpl(key = null, from, to, null, null, TransformationSpec.EmptyProvider)
+ TransitionSpecImpl(
+ key = null,
+ from,
+ to,
+ cuj = null,
+ previewTransformationSpec = null,
+ reversePreviewTransformationSpec = null,
+ TransformationSpec.EmptyProvider,
+ )
companion object {
internal val DefaultSwipeSpec =
@@ -147,6 +156,9 @@
*/
val to: ContentKey?
+ /** The CUJ covered by this transition. */
+ @CujType val cuj: Int?
+
/**
* Return a reversed version of this [TransitionSpec] for a transition going from [to] to
* [from].
@@ -213,6 +225,7 @@
override val key: TransitionKey?,
override val from: ContentKey?,
override val to: ContentKey?,
+ override val cuj: Int?,
private val previewTransformationSpec:
((TransitionState.Transition) -> TransformationSpecImpl)? =
null,
@@ -226,6 +239,7 @@
key = key,
from = to,
to = from,
+ cuj = cuj,
previewTransformationSpec = reversePreviewTransformationSpec,
reversePreviewTransformationSpec = previewTransformationSpec,
transformationSpec = { transition ->
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index fda6fab..998054e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.Transformation
+import com.android.internal.jank.Cuj.CujType
/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
@@ -64,6 +65,7 @@
fun to(
to: ContentKey,
key: TransitionKey? = null,
+ @CujType cuj: Int? = null,
preview: (TransitionBuilder.() -> Unit)? = null,
reversePreview: (TransitionBuilder.() -> Unit)? = null,
builder: TransitionBuilder.() -> Unit = {},
@@ -90,6 +92,7 @@
from: ContentKey,
to: ContentKey? = null,
key: TransitionKey? = null,
+ @CujType cuj: Int? = null,
preview: (TransitionBuilder.() -> Unit)? = null,
reversePreview: (TransitionBuilder.() -> Unit)? = null,
builder: TransitionBuilder.() -> Unit = {},
@@ -146,6 +149,9 @@
*/
var swipeSpec: SpringSpec<Float>?
+ /** The CUJ associated to this transitions. */
+ @CujType var cuj: Int?
+
/**
* Define a timestamp-based range for the transformations inside [builder].
*
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
index a164996..7ca5215 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -37,6 +37,7 @@
import com.android.compose.animation.scene.transformation.TransformationMatcher
import com.android.compose.animation.scene.transformation.TransformationRange
import com.android.compose.animation.scene.transformation.Translate
+import com.android.internal.jank.Cuj.CujType
internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
val impl = SceneTransitionsBuilderImpl().apply(builder)
@@ -52,28 +53,47 @@
override fun to(
to: ContentKey,
key: TransitionKey?,
+ @CujType cuj: Int?,
preview: (TransitionBuilder.() -> Unit)?,
reversePreview: (TransitionBuilder.() -> Unit)?,
builder: TransitionBuilder.() -> Unit,
) {
- transition(from = null, to = to, key = key, preview, reversePreview, builder)
+ transition(
+ from = null,
+ to = to,
+ key = key,
+ cuj = cuj,
+ preview = preview,
+ reversePreview = reversePreview,
+ builder = builder,
+ )
}
override fun from(
from: ContentKey,
to: ContentKey?,
key: TransitionKey?,
+ @CujType cuj: Int?,
preview: (TransitionBuilder.() -> Unit)?,
reversePreview: (TransitionBuilder.() -> Unit)?,
builder: TransitionBuilder.() -> Unit,
) {
- transition(from = from, to = to, key = key, preview, reversePreview, builder)
+ transition(
+ from = from,
+ to = to,
+ key = key,
+ cuj = cuj,
+ preview = preview,
+ reversePreview = reversePreview,
+ builder = builder,
+ )
}
private fun transition(
from: ContentKey?,
to: ContentKey?,
key: TransitionKey?,
+ @CujType cuj: Int?,
preview: (TransitionBuilder.() -> Unit)?,
reversePreview: (TransitionBuilder.() -> Unit)?,
builder: TransitionBuilder.() -> Unit,
@@ -93,9 +113,10 @@
val spec =
TransitionSpecImpl(
- key,
- from,
- to,
+ key = key,
+ from = from,
+ to = to,
+ cuj = cuj,
previewTransformationSpec = preview?.let { { t -> transformationSpec(t, it) } },
reversePreviewTransformationSpec =
reversePreview?.let { { t -> transformationSpec(t, it) } },
@@ -190,6 +211,7 @@
override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
override var swipeSpec: SpringSpec<Float>? = null
override var distance: UserActionDistance? = null
+ override var cuj: Int? = null
private val durationMillis: Int by lazy {
val spec = spec
if (spec !is DurationBasedAnimationSpec) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index e7ca511..712af56 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -32,6 +32,7 @@
import com.android.compose.animation.scene.TransformationSpec
import com.android.compose.animation.scene.TransformationSpecImpl
import com.android.compose.animation.scene.TransitionKey
+import com.android.internal.jank.Cuj.CujType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@@ -237,6 +238,11 @@
/** Whether user input is currently driving the transition. */
abstract val isUserInputOngoing: Boolean
+ /** The CUJ covered by this transition. */
+ @CujType
+ val cuj: Int?
+ get() = _cuj
+
/**
* The progress of the preview transition. This is usually in the `[0; 1]` range, but it can
* also be less than `0` or greater than `1` when using transitions with a spring
@@ -251,13 +257,15 @@
internal open val isInPreviewStage: Boolean = false
/**
- * The current [TransformationSpecImpl] associated to this transition.
+ * The current [TransformationSpecImpl] and other values associated to this transition from
+ * the spec.
*
* Important: These will be set exactly once, when this transition is
* [started][MutableSceneTransitionLayoutStateImpl.startTransition].
*/
internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty
internal var previewTransformationSpec: TransformationSpecImpl? = null
+ internal var _cuj: Int? = null
/**
* An animatable that animates from 1f to 0f. This will be used to nicely animate the sudden
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
index d1bd52b..f3be5e4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt
@@ -198,23 +198,30 @@
assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder()
// C => A. This should automatically call freezeAndAnimateToCurrentState() on bToC.
- state.startTransitionImmediately(animationScope = backgroundScope, cToA)
+ val cToAJob = state.startTransitionImmediately(animationScope = backgroundScope, cToA)
assertThat(frozenTransitions).containsExactly(aToB, bToC)
assertThat(state.finishedTransitions).isEmpty()
assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
- // Mark bToC as finished. The list of current transitions does not change because aToB is
- // still not marked as finished.
- bToC.finish()
- bToCJob.join()
- assertThat(state.finishedTransitions).containsExactly(bToC)
- assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
-
- // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions.
+ // Mark aToB and bToC as finished. The list of current transitions does not change because
+ // cToA is still running.
aToB.finish()
aToBJob.join()
+ assertThat(state.finishedTransitions).containsExactly(aToB)
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+ bToC.finish()
+ bToCJob.join()
+ assertThat(state.finishedTransitions).containsExactly(aToB, bToC)
+ assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder()
+
+ // Mark cToA as finished. This should clear all transitions and settle to idle.
+ cToA.finish()
+ cToAJob.join()
assertThat(state.finishedTransitions).isEmpty()
- assertThat(state.currentTransitions).containsExactly(cToA).inOrder()
+ assertThat(state.currentTransitions).isEmpty()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
}
@Test
@@ -473,4 +480,77 @@
"SceneKey(debugName=SceneB)"
)
}
+
+ @Test
+ fun snapToScene_multipleTransitions() = runMonotonicClockTest {
+ val state = MutableSceneTransitionLayoutState(SceneA)
+ state.startTransitionImmediately(this, transition(SceneA, SceneB))
+ state.startTransitionImmediately(this, transition(SceneB, SceneC))
+ state.snapToScene(SceneC)
+
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneC)
+ }
+
+ @Test
+ fun trackTransitionCujs() = runTest {
+ val started = mutableSetOf<TransitionState.Transition>()
+ val finished = mutableSetOf<TransitionState.Transition>()
+ val cujWhenStarting = mutableMapOf<TransitionState.Transition, Int?>()
+ val state =
+ MutableSceneTransitionLayoutState(
+ SceneA,
+ transitions {
+ // A <=> B.
+ from(SceneA, to = SceneB, cuj = 1)
+
+ // A <=> C.
+ from(SceneA, to = SceneC, cuj = 2)
+ from(SceneC, to = SceneA, cuj = 3)
+ },
+ onTransitionStart = { transition ->
+ started.add(transition)
+ cujWhenStarting[transition] = transition.cuj
+ },
+ onTransitionEnd = { finished.add(it) },
+ )
+
+ val aToB = transition(SceneA, SceneB)
+ val bToA = transition(SceneB, SceneA)
+ val aToC = transition(SceneA, SceneC)
+ val cToA = transition(SceneC, SceneA)
+
+ val animationScope = this
+ state.startTransitionImmediately(animationScope, aToB)
+ assertThat(started).containsExactly(aToB)
+ assertThat(finished).isEmpty()
+
+ state.startTransitionImmediately(animationScope, bToA)
+ assertThat(started).containsExactly(aToB, bToA)
+ assertThat(finished).isEmpty()
+
+ aToB.finish()
+ runCurrent()
+ assertThat(finished).containsExactly(aToB)
+
+ state.startTransitionImmediately(animationScope, aToC)
+ assertThat(started).containsExactly(aToB, bToA, aToC)
+ assertThat(finished).containsExactly(aToB)
+
+ state.startTransitionImmediately(animationScope, cToA)
+ assertThat(started).containsExactly(aToB, bToA, aToC, cToA)
+ assertThat(finished).containsExactly(aToB)
+
+ bToA.finish()
+ aToC.finish()
+ cToA.finish()
+ runCurrent()
+ assertThat(started).containsExactly(aToB, bToA, aToC, cToA)
+ assertThat(finished).containsExactly(aToB, bToA, aToC, cToA)
+
+ assertThat(cujWhenStarting[aToB]).isEqualTo(1)
+ assertThat(cujWhenStarting[bToA]).isEqualTo(1)
+ assertThat(cujWhenStarting[aToC]).isEqualTo(2)
+ assertThat(cujWhenStarting[cToA]).isEqualTo(3)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index fdbd0f6..7c8c6e5 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -379,8 +379,8 @@
assertThat(transition).hasProgress(0.5f)
rule.waitForIdle()
- // B and C are composed.
- rule.onNodeWithTag("aRoot").assertDoesNotExist()
+ // A, B and C are still composed given that B => C is not finished yet.
+ rule.onNodeWithTag("aRoot").assertExists()
rule.onNodeWithTag("bRoot").assertExists()
rule.onNodeWithTag("cRoot").assertExists()
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
deleted file mode 100644
index 15373d3..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shared.clocks
-
-import android.graphics.Point
-import android.view.animation.Interpolator
-import com.android.app.animation.Interpolators
-import com.android.internal.annotations.Keep
-import com.android.systemui.monet.Style as MonetStyle
-import com.android.systemui.shared.clocks.view.HorizontalAlignment
-import com.android.systemui.shared.clocks.view.VerticalAlignment
-
-/** Data format for a simple asset-defined clock */
-@Keep
-data class ClockDesign(
- val id: String,
- val name: String? = null,
- val description: String? = null,
- val thumbnail: String? = null,
- val large: ClockFace? = null,
- val small: ClockFace? = null,
- @MonetStyle.Type val colorPalette: Int? = null,
-)
-
-/** Describes a clock using layers */
-@Keep
-data class ClockFace(
- val layers: List<ClockLayer> = listOf<ClockLayer>(),
- val layerBounds: LayerBounds = LayerBounds.FIT,
- val wallpaper: String? = null,
- val faceLayout: DigitalFaceLayout? = null,
- val pickerScale: ClockFaceScaleInPicker? = ClockFaceScaleInPicker(1.0f, 1.0f),
-)
-
-@Keep data class ClockFaceScaleInPicker(val scaleX: Float, val scaleY: Float)
-
-/** Base Type for a Clock Layer */
-@Keep
-interface ClockLayer {
- /** Override of face LayerBounds setting for this layer */
- val layerBounds: LayerBounds?
-}
-
-/** Clock layer that renders a static asset */
-@Keep
-data class AssetLayer(
- /** Asset to render in this layer */
- val asset: AssetReference,
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** Clock layer that renders the time (or a component of it) using numerals */
-@Keep
-data class DigitalHandLayer(
- /** See SimpleDateFormat for timespec format info */
- val timespec: DigitalTimespec,
- val style: TextStyle,
- // adoStyle concrete type must match style,
- // cause styles will transition between style and aodStyle
- val aodStyle: TextStyle?,
- val timer: Int? = null,
- override val layerBounds: LayerBounds? = null,
- var faceLayout: DigitalFaceLayout? = null,
- // we pass 12-hour format from json, which will be converted to 24-hour format in codes
- val dateTimeFormat: String,
- val alignment: DigitalAlignment?,
- // ratio of margins to measured size, currently used for handwritten clocks
- val marginRatio: DigitalMarginRatio? = DigitalMarginRatio(),
-) : ClockLayer
-
-/** Clock layer that renders the time (or a component of it) using numerals */
-@Keep
-data class ComposedDigitalHandLayer(
- val customizedView: String? = null,
- /** See SimpleDateFormat for timespec format info */
- val digitalLayers: List<DigitalHandLayer> = listOf<DigitalHandLayer>(),
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-@Keep
-data class DigitalAlignment(
- val horizontalAlignment: HorizontalAlignment?,
- val verticalAlignment: VerticalAlignment?,
-)
-
-@Keep
-data class DigitalMarginRatio(
- val left: Float = 0F,
- val top: Float = 0F,
- val right: Float = 0F,
- val bottom: Float = 0F,
-)
-
-/** Clock layer which renders a component of the time using an analog hand */
-@Keep
-data class AnalogHandLayer(
- val timespec: AnalogTimespec,
- val tickMode: AnalogTickMode,
- val asset: AssetReference,
- val timer: Int? = null,
- val clock_pivot: Point = Point(0, 0),
- val asset_pivot: Point? = null,
- val length: Float = 1f,
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** Clock layer which renders the time using an AVD */
-@Keep
-data class AnimatedHandLayer(
- val timespec: AnalogTimespec,
- val asset: AssetReference,
- val timer: Int? = null,
- override val layerBounds: LayerBounds? = null,
-) : ClockLayer
-
-/** A collection of asset references for use in different device modes */
-@Keep
-data class AssetReference(
- val light: String,
- val dark: String,
- val doze: String? = null,
- val lightTint: String? = null,
- val darkTint: String? = null,
- val dozeTint: String? = null,
-)
-
-/**
- * Core TextStyling attributes for text clocks. Both color and sizing information can be applied to
- * either subtype.
- */
-@Keep
-interface TextStyle {
- // fontSizeScale is a scale factor applied to the default clock's font size.
- val fontSizeScale: Float?
-}
-
-/**
- * This specifies a font and styling parameters for that font. This is rendered using a text view
- * and the text animation classes used by the default clock. To ensure default value take effects,
- * all parameters MUST have a default value
- */
-@Keep
-data class FontTextStyle(
- // Font to load and use in the TextView
- val fontFamily: String? = null,
- val lineHeight: Float? = null,
- val borderWidth: String? = null,
- // ratio of borderWidth / fontSize
- val borderWidthScale: Float? = null,
- // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
- val fillColorLight: String? = null,
- // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100`
- val fillColorDark: String? = null,
- override val fontSizeScale: Float? = null,
- // used when alternate in one font file is needed
- var fontFeatureSettings: String? = null,
- val renderType: RenderType = RenderType.STROKE_TEXT,
- val outlineColor: String? = null,
- val transitionDuration: Long = -1L,
- val transitionInterpolator: InterpolatorEnum? = null,
-) : TextStyle
-
-/**
- * As an alternative to using a font, we can instead render a digital clock using a set of drawables
- * for each numeral, and optionally a colon. These drawables will be rendered directly after sizing
- * and placing them. This may be easier than generating a font file in some cases, and is provided
- * for ease of use. Unlike fonts, these are not localizable to other numeric systems (like Burmese).
- */
-@Keep
-data class LottieTextStyle(
- val numbers: List<String> = listOf(),
- // Spacing between numbers, dimension string
- val spacing: String = "0dp",
- // Colon drawable may be omitted if unused in format spec
- val colon: String? = null,
- // key is keypath name to get strokes from lottie, value is the color name to query color in
- // palette, e.g. @android:color/system_accent1_100
- val fillColorLightMap: Map<String, String>? = null,
- val fillColorDarkMap: Map<String, String>? = null,
- override val fontSizeScale: Float? = null,
- val paddingVertical: String = "0dp",
- val paddingHorizontal: String = "0dp",
-) : TextStyle
-
-/** Layer sizing mode for the clockface or layer */
-enum class LayerBounds {
- /**
- * Sized so the larger dimension matches the allocated space. This results in some of the
- * allocated space being unused.
- */
- FIT,
-
- /**
- * Sized so the smaller dimension matches the allocated space. This will clip some content to
- * the edges of the space.
- */
- FILL,
-
- /** Fills the allocated space exactly by stretching the layer */
- STRETCH,
-}
-
-/** Ticking mode for analog hands. */
-enum class AnalogTickMode {
- SWEEP,
- TICK,
-}
-
-/** Timspec options for Analog Hands. Named for tick interval. */
-enum class AnalogTimespec {
- SECONDS,
- MINUTES,
- HOURS,
- HOURS_OF_DAY,
- DAY_OF_WEEK,
- DAY_OF_MONTH,
- DAY_OF_YEAR,
- WEEK,
- MONTH,
- TIMER,
-}
-
-enum class DigitalTimespec {
- TIME_FULL_FORMAT,
- DIGIT_PAIR,
- FIRST_DIGIT,
- SECOND_DIGIT,
- DATE_FORMAT,
-}
-
-enum class DigitalFaceLayout {
- // can only use HH_PAIR, MM_PAIR from DigitalTimespec
- TWO_PAIRS_VERTICAL,
- TWO_PAIRS_HORIZONTAL,
- // can only use HOUR_FIRST_DIGIT, HOUR_SECOND_DIGIT, MINUTE_FIRST_DIGIT, MINUTE_SECOND_DIGIT
- // from DigitalTimespec, used for tabular layout when the font doesn't support tnum
- FOUR_DIGITS_ALIGN_CENTER,
- FOUR_DIGITS_HORIZONTAL,
-}
-
-enum class RenderType {
- CHANGE_WEIGHT,
- HOLLOW_TEXT,
- STROKE_TEXT,
- OUTER_OUTLINE_TEXT,
-}
-
-enum class InterpolatorEnum(factory: () -> Interpolator) {
- STANDARD({ Interpolators.STANDARD }),
- EMPHASIZED({ Interpolators.EMPHASIZED });
-
- val interpolator: Interpolator by lazy(factory)
-}
-
-fun generateDigitalLayerIdString(layer: DigitalHandLayer): String {
- return if (
- layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
- layer.timespec == DigitalTimespec.DATE_FORMAT
- ) {
- layer.timespec.toString()
- } else {
- if ("h" in layer.dateTimeFormat) {
- "HOUR" + "_" + layer.timespec.toString()
- } else {
- "MINUTE" + "_" + layer.timespec.toString()
- }
- }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index d0a32dc..9fb60c7 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -18,6 +18,7 @@
import android.graphics.Rect
import androidx.annotation.VisibleForTesting
+import com.android.app.animation.Interpolators
import com.android.systemui.log.core.Logger
import com.android.systemui.plugins.clocks.AlarmData
import com.android.systemui.plugins.clocks.ClockAnimations
@@ -29,14 +30,13 @@
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
import java.util.Locale
import java.util.TimeZone
-class ComposedDigitalLayerController(
- private val clockCtx: ClockContext,
- private val layer: ComposedDigitalHandLayer,
-) : SimpleClockLayerController {
+class ComposedDigitalLayerController(private val clockCtx: ClockContext) :
+ SimpleClockLayerController {
private val logger =
Logger(clockCtx.messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
@@ -46,14 +46,40 @@
override val view = FlexClockView(clockCtx)
init {
- layer.digitalLayers.forEach {
- val childView = SimpleDigitalClockTextView(clockCtx)
- val controller =
- SimpleDigitalHandLayerController(clockCtx, it as DigitalHandLayer, childView)
-
- view.addView(childView)
+ fun createController(cfg: LayerConfig) {
+ val controller = SimpleDigitalHandLayerController(clockCtx, cfg)
+ view.addView(controller.view)
layerControllers.add(controller)
}
+
+ val layerCfg =
+ LayerConfig(
+ style = FontTextStyle(lineHeight = 147.25f),
+ aodStyle =
+ FontTextStyle(
+ transitionInterpolator = Interpolators.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(HorizontalAlignment.CENTER, VerticalAlignment.BASELINE),
+
+ // Placeholders
+ timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ dateTimeFormat = "hh:mm",
+ )
+
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "hh")
+ )
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "hh")
+ )
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.FIRST_DIGIT, dateTimeFormat = "mm")
+ )
+ createController(
+ layerCfg.copy(timespec = DigitalTimespec.SECOND_DIGIT, dateTimeFormat = "mm")
+ )
}
private fun refreshTime() {
@@ -79,17 +105,11 @@
refreshTime()
}
- override fun onWeatherDataChanged(data: WeatherData) {
- view.onWeatherDataChanged(data)
- }
+ override fun onWeatherDataChanged(data: WeatherData) {}
- override fun onAlarmDataChanged(data: AlarmData) {
- view.onAlarmDataChanged(data)
- }
+ override fun onAlarmDataChanged(data: AlarmData) {}
- override fun onZenDataChanged(data: ZenData) {
- view.onZenDataChanged(data)
- }
+ override fun onZenDataChanged(data: ZenData) {}
override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {
view.updateAxes(axes)
@@ -123,15 +143,11 @@
view.animateCharge()
}
- override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
- view.onPositionUpdated(fromLeft, direction, fraction)
- }
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
override fun onPositionUpdated(distance: Float, fraction: Float) {}
- override fun onPickerCarouselSwiping(swipingFraction: Float) {
- view.onPickerCarouselSwiping(swipingFraction)
- }
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {}
}
override val faceEvents =
@@ -163,9 +179,8 @@
override val config =
ClockFaceConfig(
- hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay,
- hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation,
- useCustomClockScene = view.useCustomClockScene,
+ hasCustomWeatherDataDisplay = false,
+ hasCustomPositionUpdatedAnimation = true,
)
@VisibleForTesting
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index c73e1c3..f6ff3268 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -27,8 +27,6 @@
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
-import com.android.systemui.shared.clocks.view.HorizontalAlignment
-import com.android.systemui.shared.clocks.view.VerticalAlignment
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_ID = "DEFAULT"
@@ -78,8 +76,7 @@
typefaceCache,
buffers,
buffers.infraMessageBuffer,
- ),
- FLEX_DESIGN,
+ )
)
} else {
DefaultClockController(ctx, layoutInflater, resources, settings, messageBuffers)
@@ -128,119 +125,5 @@
// TODO(b/364680873): Move constant to config_clockFontFamily when shipping
Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
}
-
- val FLEX_DESIGN = run {
- val largeLayer =
- listOf(
- ComposedDigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- customizedView = "FlexClockView",
- digitalLayers =
- listOf(
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.FIRST_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "hh",
- ),
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.SECOND_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "hh",
- ),
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.FIRST_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "mm",
- ),
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.SECOND_DIGIT,
- style = FontTextStyle(lineHeight = 147.25f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- transitionInterpolator = InterpolatorEnum.EMPHASIZED,
- transitionDuration = 750,
- ),
- alignment =
- DigitalAlignment(
- HorizontalAlignment.CENTER,
- VerticalAlignment.BASELINE,
- ),
- dateTimeFormat = "mm",
- ),
- ),
- )
- )
-
- val smallLayer =
- listOf(
- DigitalHandLayer(
- layerBounds = LayerBounds.FIT,
- timespec = DigitalTimespec.TIME_FULL_FORMAT,
- style = FontTextStyle(fontSizeScale = 0.98f),
- aodStyle =
- FontTextStyle(
- fillColorLight = "#FFFFFFFF",
- outlineColor = "#00000000",
- renderType = RenderType.CHANGE_WEIGHT,
- ),
- alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
- dateTimeFormat = "h:mm",
- )
- )
-
- ClockDesign(
- id = DEFAULT_CLOCK_ID,
- name = "@string/clock_default_name",
- description = "@string/clock_default_description",
- large = ClockFace(layers = largeLayer),
- small = ClockFace(layers = smallLayer),
- )
- }
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index 7f01fd7..aed3a2d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -32,21 +32,16 @@
import java.util.TimeZone
/** Controller for the default flex clock */
-class FlexClockController(
- private val clockCtx: ClockContext,
- val design: ClockDesign, // TODO(b/364680879): Remove when done inlining
-) : ClockController {
+class FlexClockController(private val clockCtx: ClockContext) : ClockController {
override val smallClock =
FlexClockFaceController(
clockCtx.copy(messageBuffer = clockCtx.messageBuffers.smallClockMessageBuffer),
- design.small ?: design.large!!,
isLargeClock = false,
)
override val largeClock =
FlexClockFaceController(
clockCtx.copy(messageBuffer = clockCtx.messageBuffers.largeClockMessageBuffer),
- design.large ?: design.small!!,
isLargeClock = true,
)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index 4a47f1b..827bd68 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -35,17 +35,14 @@
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
import java.util.Locale
import java.util.TimeZone
import kotlin.math.max
// TODO(b/364680879): Merge w/ ComposedDigitalLayerController
-class FlexClockFaceController(
- clockCtx: ClockContext,
- face: ClockFace,
- private val isLargeClock: Boolean,
-) : ClockFaceController {
+class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: Boolean) :
+ ClockFaceController {
override val view: View
get() = layerController.view
@@ -59,19 +56,12 @@
val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
init {
- val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
- lp.gravity = Gravity.CENTER
-
- val layer = face.layers[0]
-
layerController =
- if (isLargeClock) {
- ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer)
- } else {
- val childView = SimpleDigitalClockTextView(clockCtx)
- SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView)
- }
- layerController.view.layoutParams = lp
+ if (isLargeClock) ComposedDigitalLayerController(clockCtx)
+ else SimpleDigitalHandLayerController(clockCtx, SMALL_LAYER_CONFIG)
+
+ layerController.view.layoutParams =
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT).apply { gravity = Gravity.CENTER }
}
/** See documentation at [FlexClockView.offsetGlyphsForStepClockAnimation]. */
@@ -227,10 +217,6 @@
}
override fun onPickerCarouselSwiping(swipingFraction: Float) {
- face.pickerScale?.let {
- view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX
- view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY
- }
if (isLargeClock && !(view as FlexClockView).isAlignedWithScreen()) {
view.translationY = keyguardLargeClockTopMargin / 2F * swipingFraction
}
@@ -248,4 +234,15 @@
// TODO(b/378128811) port stepping animation
}
}
+
+ companion object {
+ val SMALL_LAYER_CONFIG =
+ LayerConfig(
+ timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ style = FontTextStyle(fontSizeScale = 0.98f),
+ aodStyle = FontTextStyle(),
+ alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+ dateTimeFormat = "h:mm",
+ )
+ }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
deleted file mode 100644
index 6e1b9aa..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shared.clocks
-
-import android.content.Context
-import android.view.View.MeasureSpec.EXACTLY
-import android.widget.RelativeLayout
-import androidx.core.view.children
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
-
-class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) :
- RelativeLayout(context) {
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- // For migrate_clocks_to_blueprint, mode is EXACTLY
- // when the flag is turned off, we won't execute this codes
- if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
- if (
- faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL ||
- faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER
- ) {
- val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F
- children.forEach {
- // The assumption here is the height of text view is linear to font size
- (it as SimpleDigitalClockView).applyTextSize(
- constrainedHeight,
- constrainedByHeight = true,
- )
- }
- }
- }
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
- }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index ebac4b24..82fc3501 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -17,8 +17,8 @@
package com.android.systemui.shared.clocks
import android.graphics.Rect
-import android.view.View
import android.view.ViewGroup
+import android.view.animation.Interpolator
import android.widget.RelativeLayout
import androidx.annotation.VisibleForTesting
import com.android.systemui.customization.R
@@ -32,22 +32,56 @@
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
-import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import com.android.systemui.shared.clocks.view.VerticalAlignment
import java.util.Locale
import java.util.TimeZone
private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
-open class SimpleDigitalHandLayerController<T>(
- private val clockCtx: ClockContext,
- private val layer: DigitalHandLayer,
- override val view: T,
-) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
- private val logger = Logger(clockCtx.messageBuffer, TAG)
- val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
+// TODO(b/364680879): The remains of ClockDesign. Cut further.
+data class LayerConfig(
+ val style: FontTextStyle,
+ val aodStyle: FontTextStyle,
+ val alignment: DigitalAlignment,
+ val timespec: DigitalTimespec,
+ val dateTimeFormat: String,
+) {
+ fun generateDigitalLayerIdString(): String {
+ return when {
+ timespec == DigitalTimespec.TIME_FULL_FORMAT -> "$timespec"
+ "h" in dateTimeFormat -> "HOUR_$timespec"
+ else -> "MINUTE_$timespec"
+ }
+ }
+}
- @VisibleForTesting
- fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
+data class DigitalAlignment(
+ val horizontalAlignment: HorizontalAlignment?,
+ val verticalAlignment: VerticalAlignment?,
+)
+
+data class FontTextStyle(
+ val lineHeight: Float? = null,
+ val fontSizeScale: Float? = null,
+ val transitionDuration: Long = -1L,
+ val transitionInterpolator: Interpolator? = null,
+)
+
+enum class DigitalTimespec {
+ TIME_FULL_FORMAT,
+ FIRST_DIGIT,
+ SECOND_DIGIT,
+}
+
+open class SimpleDigitalHandLayerController(
+ private val clockCtx: ClockContext,
+ private val layerCfg: LayerConfig,
+) : SimpleClockLayerController {
+ override val view = SimpleDigitalClockTextView(clockCtx)
+ private val logger = Logger(clockCtx.messageBuffer, TAG)
+ val timespec = DigitalTimespecHandler(layerCfg.timespec, layerCfg.dateTimeFormat)
@VisibleForTesting
override var fakeTimeMills: Long?
@@ -65,145 +99,17 @@
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
- if (layer.alignment != null) {
- layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
- layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
- }
- view.applyStyles(layer.style, layer.aodStyle)
+ layerCfg.alignment.verticalAlignment?.let { view.verticalAlignment = it }
+ layerCfg.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
+ view.applyStyles(layerCfg.style, layerCfg.aodStyle)
view.id =
clockCtx.resources.getIdentifier(
- generateDigitalLayerIdString(layer),
+ layerCfg.generateDigitalLayerIdString(),
"id",
clockCtx.context.getPackageName(),
)
}
- fun applyLayout(layout: DigitalFaceLayout?) {
- when (layout) {
- DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER,
- DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout)
- DigitalFaceLayout.TWO_PAIRS_HORIZONTAL,
- DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout)
- else -> {
- // one view always use FrameLayout
- // no need to change here
- }
- }
- applyMargin()
- }
-
- private fun applyMargin() {
- if (view.layoutParams is RelativeLayout.LayoutParams) {
- val lp = view.layoutParams as RelativeLayout.LayoutParams
- layer.marginRatio?.let {
- lp.setMargins(
- /* left = */ (it.left * view.measuredWidth).toInt(),
- /* top = */ (it.top * view.measuredHeight).toInt(),
- /* right = */ (it.right * view.measuredWidth).toInt(),
- /* bottom = */ (it.bottom * view.measuredHeight).toInt(),
- )
- }
- view.layoutParams = lp
- }
- }
-
- private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) {
- val lp = view.layoutParams as RelativeLayout.LayoutParams
- lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
- if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) {
- when (view.id) {
- R.id.HOUR_DIGIT_PAIR -> {
- lp.addRule(RelativeLayout.CENTER_VERTICAL)
- lp.addRule(RelativeLayout.ALIGN_PARENT_START)
- }
- R.id.MINUTE_DIGIT_PAIR -> {
- lp.addRule(RelativeLayout.CENTER_VERTICAL)
- lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
- }
- else -> {
- throw Exception("cannot apply two pairs layout to view ${view.id}")
- }
- }
- } else {
- when (view.id) {
- R.id.HOUR_DIGIT_PAIR -> {
- lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
- lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
- }
- R.id.MINUTE_DIGIT_PAIR -> {
- lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
- lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR)
- }
- else -> {
- throw Exception("cannot apply two pairs layout to view ${view.id}")
- }
- }
- }
- view.layoutParams = lp
- }
-
- private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) {
- val lp = view.layoutParams as RelativeLayout.LayoutParams
- when (fourDigitsfaceLayout) {
- DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> {
- when (view.id) {
- R.id.HOUR_FIRST_DIGIT -> {
- lp.addRule(RelativeLayout.ALIGN_PARENT_START)
- lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
- }
- R.id.HOUR_SECOND_DIGIT -> {
- lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
- lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT)
- }
- R.id.MINUTE_FIRST_DIGIT -> {
- lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT)
- lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT)
- }
- R.id.MINUTE_SECOND_DIGIT -> {
- lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT)
- lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT)
- }
- else -> {
- throw Exception("cannot apply four digits layout to view ${view.id}")
- }
- }
- }
- DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> {
- when (view.id) {
- R.id.HOUR_FIRST_DIGIT -> {
- lp.addRule(RelativeLayout.CENTER_VERTICAL)
- lp.addRule(RelativeLayout.ALIGN_PARENT_START)
- }
- R.id.HOUR_SECOND_DIGIT -> {
- lp.addRule(RelativeLayout.CENTER_VERTICAL)
- lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
- }
- R.id.MINUTE_FIRST_DIGIT -> {
- lp.addRule(RelativeLayout.CENTER_VERTICAL)
- lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT)
- }
- R.id.MINUTE_SECOND_DIGIT -> {
- lp.addRule(RelativeLayout.CENTER_VERTICAL)
- lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT)
- }
- else -> {
- throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}")
- }
- }
- }
- else -> {
- throw IllegalArgumentException(
- "applyFourDigitsLayout function should not " +
- "have parameters as ${layer.faceLayout}"
- )
- }
- }
- if (lp == view.layoutParams) {
- return
- }
- view.layoutParams = lp
- }
-
fun refreshTime() {
timespec.updateTime()
val text = timespec.getDigitString()
@@ -248,7 +154,6 @@
override val animations =
object : ClockAnimations {
override fun enter() {
- applyLayout(layer.faceLayout)
refreshTime()
}
@@ -264,7 +169,6 @@
}
override fun fold(fraction: Float) {
- applyLayout(layer.faceLayout)
refreshTime()
}
@@ -283,17 +187,13 @@
object : ClockFaceEvents {
override fun onTimeTick() {
refreshTime()
- if (
- layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
- layer.timespec == DigitalTimespec.DATE_FORMAT
- ) {
+ if (layerCfg.timespec == DigitalTimespec.TIME_FULL_FORMAT) {
view.contentDescription = timespec.getContentDescription()
}
}
override fun onFontSettingChanged(fontSizePx: Float) {
view.applyTextSize(fontSizePx)
- applyMargin()
}
override fun onThemeChanged(theme: ThemeConfig) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
index ed6a403..37db783 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -25,9 +25,7 @@
import java.util.Locale
import java.util.TimeZone
-open class TimespecHandler(
- val cal: Calendar,
-) {
+open class TimespecHandler(val cal: Calendar) {
var timeZone: TimeZone
get() = cal.timeZone
set(value) {
@@ -82,10 +80,7 @@
}
private fun updateSimpleDateFormat(locale: Locale): DateFormat {
- if (
- locale.language.equals(Locale.ENGLISH.language) ||
- timespec != DigitalTimespec.DATE_FORMAT
- ) {
+ if (locale.language.equals(Locale.ENGLISH.language)) {
// force date format in English, and time format to use format defined in json
return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
} else {
@@ -97,24 +92,18 @@
return when (timespec) {
DigitalTimespec.TIME_FULL_FORMAT ->
SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
- DigitalTimespec.DATE_FORMAT ->
- SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
- else -> {
- null
- }
+ else -> null
}
}
private fun applyPattern() {
val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH")
val format = if (is24Hr) timeFormat24Hour else timeFormat
- if (timespec != DigitalTimespec.DATE_FORMAT) {
- (dateFormat as SimpleDateFormat).applyPattern(format)
- (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
- if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
- else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
- )
- }
+ (dateFormat as SimpleDateFormat).applyPattern(format)
+ (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
+ if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
+ else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
+ )
}
private fun getSingleDigit(): String {
@@ -122,7 +111,7 @@
val text = dateFormat.format(cal.time).toString()
return text.substring(
if (isFirstDigit) 0 else text.length - 1,
- if (isFirstDigit) text.length - 1 else text.length
+ if (isFirstDigit) text.length - 1 else text.length,
)
}
@@ -130,27 +119,16 @@
return when (timespec) {
DigitalTimespec.FIRST_DIGIT,
DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
- DigitalTimespec.DIGIT_PAIR -> {
- dateFormat.format(cal.time).toString()
- }
- DigitalTimespec.TIME_FULL_FORMAT -> {
- dateFormat.format(cal.time).toString()
- }
- DigitalTimespec.DATE_FORMAT -> {
- dateFormat.format(cal.time).toString().uppercase()
- }
+ DigitalTimespec.TIME_FULL_FORMAT -> dateFormat.format(cal.time).toString()
}
}
fun getContentDescription(): String? {
return when (timespec) {
- DigitalTimespec.TIME_FULL_FORMAT,
- DigitalTimespec.DATE_FORMAT -> {
+ DigitalTimespec.TIME_FULL_FORMAT -> {
contentDescriptionFormat?.format(cal.time).toString()
}
- else -> {
- return null
- }
+ else -> return null
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
deleted file mode 100644
index d4eb767..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shared.clocks.view
-
-import android.graphics.Canvas
-import android.graphics.Point
-import android.view.View
-import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.log.core.Logger
-import com.android.systemui.plugins.clocks.AlarmData
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.plugins.clocks.WeatherData
-import com.android.systemui.plugins.clocks.ZenData
-import com.android.systemui.shared.clocks.ClockContext
-import com.android.systemui.shared.clocks.LogUtil
-import java.util.Locale
-
-// TODO(b/364680879): Merge w/ only subclass FlexClockView
-abstract class DigitalClockFaceView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
- protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
- get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
-
- abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
-
- @VisibleForTesting
- var isAnimationEnabled = true
- set(value) {
- field = value
- digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
- }
-
- var dozeFraction: Float = 0F
- set(value) {
- field = value
- digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
- }
-
- val dozeControlState = DozeControlState()
-
- var isReactiveTouchInteractionEnabled = false
- set(value) {
- field = value
- }
-
- open val text: String?
- get() = null
-
- open fun refreshTime() = logger.d("refreshTime()")
-
- override fun invalidate() {
- logger.d("invalidate()")
- super.invalidate()
- }
-
- override fun requestLayout() {
- logger.d("requestLayout()")
- super.requestLayout()
- }
-
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- logger.d("onMeasure()")
- calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { setMeasuredDimension(it.x, it.y) }
- ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
- calculateLeftTopPosition()
- dozeControlState.animateReady = true
- }
-
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- logger.d("onLayout()")
- super.onLayout(changed, left, top, right, bottom)
- }
-
- override fun onDraw(canvas: Canvas) {
- text?.let { logger.d({ "onDraw($str1)" }) { str1 = it } } ?: run { logger.d("onDraw()") }
- super.onDraw(canvas)
- }
-
- /*
- * Called in onMeasure to generate width/height overrides to the normal measuring logic. A null
- * result causes the normal view measuring logic to execute.
- */
- protected open fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? = null
-
- protected open fun calculateLeftTopPosition() {}
-
- override fun addView(child: View?) {
- if (child == null) return
- logger.d({ "addView($str1 @$int1)" }) {
- str1 = child::class.simpleName!!
- int1 = child.id
- }
- super.addView(child)
- if (child is SimpleDigitalClockTextView) {
- digitalClockTextViewMap[child.id] = child
- }
- child.setWillNotDraw(true)
- }
-
- open fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
- digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
- }
-
- open fun animateCharge() {
- digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
- }
-
- open fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
-
- fun updateColor(color: Int) {
- digitalClockTextViewMap.forEach { _, view -> view.updateColor(color) }
- invalidate()
- }
-
- fun updateAxes(axes: List<ClockFontAxisSetting>) {
- digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
- requestLayout()
- }
-
- fun onFontSettingChanged(fontSizePx: Float) {
- digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
- }
-
- open val hasCustomWeatherDataDisplay
- get() = false
-
- open val hasCustomPositionUpdatedAnimation
- get() = false
-
- /** True if it's large weather clock, will use weatherBlueprint in compose */
- open val useCustomClockScene
- get() = false
-
- open fun onLocaleChanged(locale: Locale) {}
-
- open fun onWeatherDataChanged(data: WeatherData) {}
-
- open fun onAlarmDataChanged(data: AlarmData) {}
-
- open fun onZenDataChanged(data: ZenData) {}
-
- open fun onPickerCarouselSwiping(swipingFraction: Float) {}
-
- open fun isAlignedWithScreen(): Boolean = false
-
- /**
- * animateDoze needs correct translate value, which is calculated in onMeasure so we need to
- * delay this animation when we get correct values
- */
- class DozeControlState {
- var animateDoze: () -> Unit = {}
- set(value) {
- if (animateReady) {
- value()
- field = {}
- } else {
- field = value
- }
- }
-
- var animateReady = false
- set(value) {
- if (value) {
- animateDoze()
- animateDoze = {}
- }
- field = value
- }
- }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index faef18c..c40bb9a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -22,11 +22,16 @@
import android.util.MathUtils.constrainedMap
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.RelativeLayout
+import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators
import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.shared.clocks.ClockContext
import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.LogUtil
import java.util.Locale
import kotlin.math.abs
import kotlin.math.max
@@ -34,14 +39,38 @@
fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
-class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
- override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
+class FlexClockView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
+ protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
+ get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+ @VisibleForTesting
+ var isAnimationEnabled = true
+ set(value) {
+ field = value
+ digitalClockTextViewMap.forEach { _, view -> view.isAnimationEnabled = value }
+ }
+
+ var dozeFraction: Float = 0F
+ set(value) {
+ field = value
+ digitalClockTextViewMap.forEach { _, view -> view.dozeFraction = field }
+ }
+
+ var isReactiveTouchInteractionEnabled = false
+ set(value) {
+ field = value
+ }
+
+ var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
private val digitLeftTopMap = mutableMapOf<Int, Point>()
private var maxSingleDigitSize = Point(-1, -1)
private val lockscreenTranslate = Point(0, 0)
private var aodTranslate = Point(0, 0)
+ private var onAnimateDoze: (() -> Unit)? = null
+ private var isDozeReadyToAnimate = false
+
// Does the current language have mono vertical size when displaying numerals
private var isMonoVerticalNumericLineSpacing = true
@@ -57,13 +86,7 @@
private val digitOffsets = mutableMapOf<Int, Float>()
- override fun addView(child: View?) {
- super.addView(child)
- (child as SimpleDigitalClockTextView).digitTranslateAnimator =
- DigitTranslateAnimator(::invalidate)
- }
-
- protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+ protected fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point? {
maxSingleDigitSize = Point(-1, -1)
val bottomLocation: (textView: SimpleDigitalClockTextView) -> Int = { textView ->
if (isMonoVerticalNumericLineSpacing) {
@@ -85,7 +108,7 @@
)
}
- protected override fun calculateLeftTopPosition() {
+ protected fun calculateLeftTopPosition() {
digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0)
digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y)
@@ -96,13 +119,57 @@
}
}
- override fun refreshTime() {
- super.refreshTime()
+ override fun addView(child: View?) {
+ if (child == null) return
+ logger.d({ "addView($str1 @$int1)" }) {
+ str1 = child::class.simpleName!!
+ int1 = child.id
+ }
+
+ super.addView(child)
+ (child as? SimpleDigitalClockTextView)?.let {
+ it.digitTranslateAnimator = DigitTranslateAnimator(::invalidate)
+ digitalClockTextViewMap[child.id] = child
+ }
+ child.setWillNotDraw(true)
+ }
+
+ fun refreshTime() {
+ logger.d("refreshTime()")
digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
}
+ override fun invalidate() {
+ logger.d("invalidate()")
+ super.invalidate()
+ }
+
+ override fun requestLayout() {
+ logger.d("requestLayout()")
+ super.requestLayout()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ logger.d("onMeasure()")
+ calculateSize(widthMeasureSpec, heightMeasureSpec)?.let { size ->
+ setMeasuredDimension(size.x, size.y)
+ } ?: run { super.onMeasure(widthMeasureSpec, heightMeasureSpec) }
+ calculateLeftTopPosition()
+
+ isDozeReadyToAnimate = true
+ onAnimateDoze?.invoke()
+ onAnimateDoze = null
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ logger.d("onLayout()")
+ super.onLayout(changed, left, top, right, bottom)
+ }
+
override fun onDraw(canvas: Canvas) {
+ logger.d("onDraw()")
super.onDraw(canvas)
+
digitalClockTextViewMap.forEach { (id, textView) ->
// save canvas location in anticipation of restoration later
canvas.save()
@@ -117,14 +184,30 @@
}
}
- override fun onLocaleChanged(locale: Locale) {
+ fun isAlignedWithScreen(): Boolean = false
+
+ fun onLocaleChanged(locale: Locale) {
updateLocale(locale)
requestLayout()
}
- override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
- dozeControlState.animateDoze = {
- super.animateDoze(isDozing, isAnimated)
+ fun updateColor(color: Int) {
+ digitalClockTextViewMap.forEach { _, view -> view.updateColor(color) }
+ invalidate()
+ }
+
+ fun updateAxes(axes: List<ClockFontAxisSetting>) {
+ digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) }
+ requestLayout()
+ }
+
+ fun onFontSettingChanged(fontSizePx: Float) {
+ digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) }
+ }
+
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ fun executeDozeAnimation() {
+ digitalClockTextViewMap.forEach { _, view -> view.animateDoze(isDozing, isAnimated) }
if (maxSingleDigitSize.x < 0 || maxSingleDigitSize.y < 0) {
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
}
@@ -150,10 +233,13 @@
}
}
}
+
+ if (isDozeReadyToAnimate) executeDozeAnimation()
+ else onAnimateDoze = { executeDozeAnimation() }
}
- override fun animateCharge() {
- super.animateCharge()
+ fun animateCharge() {
+ digitalClockTextViewMap.forEach { _, view -> view.animateCharge() }
digitalClockTextViewMap.forEach { (id, textView) ->
textView.digitTranslateAnimator?.let {
it.animatePosition(
@@ -301,7 +387,7 @@
// Add language tags below that do not have vertically mono spaced numerals
private val NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES =
setOf(
- "my", // Burmese
+ "my" // Burmese
)
// Use the sign of targetTranslation to control the direction of digit translation
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index cef24e9..0f8ca94 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -44,19 +44,30 @@
import com.android.systemui.shared.clocks.DimensionParser
import com.android.systemui.shared.clocks.FontTextStyle
import com.android.systemui.shared.clocks.LogUtil
-import com.android.systemui.shared.clocks.RenderType
-import com.android.systemui.shared.clocks.TextStyle
import java.lang.Thread
import kotlin.math.max
import kotlin.math.min
private val TAG = SimpleDigitalClockTextView::class.simpleName!!
+enum class VerticalAlignment {
+ TOP,
+ BOTTOM,
+ BASELINE, // default
+ CENTER,
+}
+
+enum class HorizontalAlignment {
+ LEFT,
+ RIGHT,
+ CENTER, // default
+}
+
@SuppressLint("AppCompatCustomView")
open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) :
- TextView(clockCtx.context, attrs), SimpleDigitalClockView {
+ TextView(clockCtx.context, attrs) {
val lockScreenPaint = TextPaint()
- override lateinit var textStyle: FontTextStyle
+ lateinit var textStyle: FontTextStyle
lateinit var aodStyle: FontTextStyle
private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION)
@@ -98,25 +109,20 @@
TextAnimator(layout, typefaceCache, invalidateCb)
}
- override var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
- override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
- override var isAnimationEnabled = true
- override var dozeFraction: Float = 0F
+ var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
+ var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
+ var isAnimationEnabled = true
+ var dozeFraction: Float = 0F
set(value) {
field = value
invalidate()
}
- // Have to passthrough to unify View with SimpleDigitalClockView
- override var text: String
- get() = super.getText().toString()
- set(value) = super.setText(value)
-
var textBorderWidth = 0F
var baselineFromMeasure = 0
var lockscreenColor = Color.WHITE
- override fun updateColor(color: Int) {
+ fun updateColor(color: Int) {
lockscreenColor = color
lockScreenPaint.color = lockscreenColor
if (dozeFraction < 1f) {
@@ -125,7 +131,7 @@
invalidate()
}
- override fun updateAxes(axes: List<ClockFontAxisSetting>) {
+ fun updateAxes(axes: List<ClockFontAxisSetting>) {
lsFontVariation = ClockFontAxisSetting.toFVar(axes + OPTICAL_SIZE_AXIS)
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
@@ -226,24 +232,6 @@
canvas.translate(it.updatedTranslate.x.toFloat(), it.updatedTranslate.y.toFloat())
}
- if (aodStyle.renderType == RenderType.HOLLOW_TEXT) {
- canvas.saveLayer(
- -translation.x.toFloat(),
- -translation.y.toFloat(),
- (-translation.x + measuredWidth).toFloat(),
- (-translation.y + measuredHeight).toFloat(),
- null,
- )
- canvas.saveLayer(
- -translation.x.toFloat(),
- -translation.y.toFloat(),
- (-translation.x + measuredWidth).toFloat(),
- (-translation.y + measuredHeight).toFloat(),
- PORTER_DUFF_XFER_MODE_PAINT,
- )
- canvas.restore()
- canvas.restore()
- }
textAnimator.draw(canvas)
digitTranslateAnimator?.let {
@@ -258,15 +246,15 @@
override fun invalidate() {
logger.d("invalidate()")
super.invalidate()
- (parent as? DigitalClockFaceView)?.invalidate()
+ (parent as? FlexClockView)?.invalidate()
}
- override fun refreshTime() {
+ fun refreshTime() {
logger.d("refreshTime()")
refreshText()
}
- override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
if (!this::textAnimator.isInitialized) return
textAnimator.setTextStyle(
animate = isAnimated && isAnimationEnabled,
@@ -279,7 +267,7 @@
updateTextBoundsForTextAnimator()
}
- override fun animateCharge() {
+ fun animateCharge() {
if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
// Skip charge animation if dozing animation is already playing.
return
@@ -419,27 +407,15 @@
return updateXtranslation(localTranslation, interpolatedTextBounds)
}
- override fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) {
- this.textStyle = textStyle as FontTextStyle
- val typefaceName = "fonts/" + textStyle.fontFamily
+ fun applyStyles(textStyle: FontTextStyle, aodStyle: FontTextStyle?) {
+ this.textStyle = textStyle
lockScreenPaint.strokeJoin = Paint.Join.ROUND
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
- textStyle.fontFeatureSettings?.let {
- lockScreenPaint.fontFeatureSettings = it
- fontFeatureSettings = it
- }
typeface = lockScreenPaint.typeface
textStyle.lineHeight?.let { lineHeight = it.toInt() }
- // borderWidth in textStyle and aodStyle is used to draw,
- // strokeWidth in lockScreenPaint is used to measure and get enough space for the text
- textStyle.borderWidth?.let { textBorderWidth = parser.convert(it) }
- if (aodStyle != null && aodStyle is FontTextStyle) {
- this.aodStyle = aodStyle
- } else {
- this.aodStyle = textStyle.copy()
- }
- this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it.interpolator }
+ this.aodStyle = aodStyle ?: textStyle.copy()
+ this.aodStyle.transitionInterpolator?.let { aodDozingInterpolator = it }
lockScreenPaint.strokeWidth = textBorderWidth
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
setInterpolatorPaint()
@@ -448,7 +424,7 @@
}
// When constrainedByHeight is on, targetFontSizePx is the constrained height of textView
- override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) {
+ fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false) {
val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
aodFontSizePx =
@@ -463,7 +439,6 @@
val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
}
- textStyle.borderWidthScale?.let { textBorderWidth = fontSizePx * it }
lockScreenPaint.strokeWidth = textBorderWidth
recomputeMaxSingleDigitSizes()
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
deleted file mode 100644
index e8be28f..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shared.clocks.view
-
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.TextStyle
-
-interface SimpleDigitalClockView {
- var text: String
- var verticalAlignment: VerticalAlignment
- var horizontalAlignment: HorizontalAlignment
- var dozeFraction: Float
- val textStyle: TextStyle
- @VisibleForTesting var isAnimationEnabled: Boolean
-
- fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?)
-
- fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
-
- fun updateColor(color: Int)
-
- fun updateAxes(axes: List<ClockFontAxisSetting>)
-
- fun refreshTime()
-
- fun animateCharge()
-
- fun animateDoze(isDozing: Boolean, isAnimated: Boolean)
-}
-
-enum class VerticalAlignment {
- TOP,
- BOTTOM,
- BASELINE, // default
- CENTER,
-}
-
-enum class HorizontalAlignment {
- LEFT,
- RIGHT,
- CENTER, // default
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
index e659ef2..698fac1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepositoryTest.kt
@@ -18,7 +18,9 @@
import android.content.Context
import android.content.Context.INPUT_SERVICE
+import android.content.Intent
import android.hardware.input.InputGestureData
+import android.hardware.input.InputManager
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.fakeInputManager
import android.platform.test.annotations.EnableFlags
@@ -27,9 +29,12 @@
import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyboard.shortcut.customInputGesturesRepository
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.settings.userTracker
@@ -48,18 +53,41 @@
@EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
class CustomInputGesturesRepositoryTest : SysuiTestCase() {
- private val mockUserContext: Context = mock()
+ private val primaryUserContext: Context = mock()
+ private val secondaryUserContext: Context = mock()
+ private var activeUserContext: Context = primaryUserContext
+
private val kosmos = testKosmos().also {
- it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
+ it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { activeUserContext })
}
private val inputManager = kosmos.fakeInputManager.inputManager
+ private val broadcastDispatcher = kosmos.broadcastDispatcher
+ private val inputManagerForSecondaryUser: InputManager = mock()
private val testScope = kosmos.testScope
+ private val testHelper = kosmos.shortcutHelperTestHelper
private val customInputGesturesRepository = kosmos.customInputGesturesRepository
@Before
- fun setup(){
- whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+ fun setup() {
+ activeUserContext = primaryUserContext
+ whenever(primaryUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
+ whenever(secondaryUserContext.getSystemService(INPUT_SERVICE))
+ .thenReturn(inputManagerForSecondaryUser)
+ }
+
+ @Test
+ fun customInputGestures_emitsNewUsersInputGesturesWhenUserIsSwitch() {
+ testScope.runTest {
+ setCustomInputGesturesForPrimaryUser(allAppsInputGestureData)
+ setCustomInputGesturesForSecondaryUser(goHomeInputGestureData)
+
+ val inputGestures by collectLastValue(customInputGesturesRepository.customInputGestures)
+ assertThat(inputGestures).containsExactly(allAppsInputGestureData)
+
+ switchToSecondaryUser()
+ assertThat(inputGestures).containsExactly(goHomeInputGestureData)
+ }
}
@Test
@@ -115,4 +143,24 @@
}
}
+ private fun setCustomInputGesturesForPrimaryUser(vararg inputGesture: InputGestureData) {
+ whenever(
+ inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+ ).thenReturn(inputGesture.toList())
+ }
+
+ private fun setCustomInputGesturesForSecondaryUser(vararg inputGesture: InputGestureData) {
+ whenever(
+ inputManagerForSecondaryUser.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+ ).thenReturn(inputGesture.toList())
+ }
+
+ private fun switchToSecondaryUser() {
+ activeUserContext = secondaryUserContext
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_SWITCHED)
+ )
+ }
+
}
\ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index b29a5f4..9e8713b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.shade.data.repository.FlingInfo
import com.android.systemui.shade.data.repository.fakeShadeRepository
@@ -47,7 +48,6 @@
import org.junit.runner.RunWith
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
-import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -55,9 +55,8 @@
class FromLockscreenTransitionInteractorTest : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
- this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository(
- testScope = testScope,
- ))
+ this.fakeKeyguardTransitionRepository =
+ spy(FakeKeyguardTransitionRepository(testScope = testScope))
}
private val testScope = kosmos.testScope
@@ -181,6 +180,12 @@
underTest.start()
assertThatRepository(transitionRepository).noTransitionsStarted()
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = testScope,
+ )
+
keyguardRepository.setKeyguardDismissible(true)
runCurrent()
shadeRepository.setCurrentFling(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
index ea2b3cd..605a5d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
@@ -21,6 +21,7 @@
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.IRemoteAnimationFinishedCallback
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -38,10 +39,13 @@
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -222,4 +226,22 @@
underTest.setSurfaceBehindVisibility(false)
verify(keyguardTransitions).startKeyguardTransition(eq(true), any())
}
+
+ @Test
+ fun remoteAnimationInstantlyFinished_ifDismissTransitionNotStarted() {
+ val mockedCallback = mock<IRemoteAnimationFinishedCallback>()
+ whenever(keyguardDismissTransitionInteractor.startDismissKeyguardTransition(any()))
+ .thenReturn(false)
+
+ underTest.onKeyguardGoingAwayRemoteAnimationStart(
+ transit = 0,
+ apps = emptyArray(),
+ wallpapers = emptyArray(),
+ nonApps = emptyArray(),
+ finishedCallback = mockedCallback,
+ )
+
+ verify(mockedCallback).onAnimationFinished()
+ verifyNoMoreInteractions(mockedCallback)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
index a36e0ea..9bae7bd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/TaskbarDelegateTest.kt
@@ -1,6 +1,7 @@
package com.android.systemui.navigationbar
import android.app.ActivityManager
+import android.os.Handler
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -61,6 +62,7 @@
@Mock lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock lateinit var mStatusBarStateController: StatusBarStateController
@Mock lateinit var mDisplayTracker: DisplayTracker
+ @Mock lateinit var mHandler: Handler
@Before
fun setup() {
@@ -69,6 +71,11 @@
`when`(mLightBarControllerFactory.create(any())).thenReturn(mLightBarTransitionController)
`when`(mNavBarHelper.currentSysuiState).thenReturn(mCurrentSysUiState)
`when`(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState)
+ `when`(mHandler.post(any())).thenAnswer {
+ (it.arguments[0] as Runnable).run()
+ true
+ }
+
mTaskStackChangeListeners = TaskStackChangeListeners.getTestInstance()
mTaskbarDelegate =
TaskbarDelegate(
@@ -76,6 +83,7 @@
mLightBarControllerFactory,
mStatusBarKeyguardViewManager,
mStatusBarStateController,
+ mHandler,
)
mTaskbarDelegate.setDependencies(
mCommandQueue,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index b5043ce..fe44c3e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -39,7 +39,7 @@
@Before
fun setUp() {
- underTest = ShadeRepositoryImpl()
+ underTest = ShadeRepositoryImpl(testScope)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 4a3be44..20474c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -81,6 +81,7 @@
import com.android.systemui.flags.DisableSceneContainer;
import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.log.LogWtfHandlerRule;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
@@ -90,6 +91,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
@@ -115,6 +117,7 @@
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
@@ -169,6 +172,10 @@
@Mock
private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
@Mock
+ private Lazy<KeyguardInteractor> mKeyguardInteractorLazy;
+ @Mock
+ private KeyguardInteractor mKeyguardInteractor;
+ @Mock
private StateFlow<DeviceUnlockStatus> mDeviceUnlockStatusStateFlow;
private UserInfo mCurrentUser;
@@ -181,6 +188,7 @@
private NotificationEntry mSecondaryUserNotif;
private NotificationEntry mWorkProfileNotif;
private NotificationEntry mSensitiveContentNotif;
+ private long mSensitiveNotifPostTime;
private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
@@ -246,13 +254,17 @@
mSensitiveContentNotif = new NotificationEntryBuilder()
.setNotification(notifWithPrivateVisibility)
.setUser(new UserHandle(mCurrentUser.id))
+ .setPostTime(System.currentTimeMillis())
.build();
mSensitiveContentNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking())
.setChannel(channel)
.setSensitiveContent(true)
.setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
+ mSensitiveNotifPostTime = mSensitiveContentNotif.getSbn().getPostTime();
when(mNotifCollection.getEntry(mWorkProfileNotif.getKey())).thenReturn(mWorkProfileNotif);
-
+ when(mKeyguardInteractorLazy.get()).thenReturn(mKeyguardInteractor);
+ when(mKeyguardInteractor.isKeyguardDismissible())
+ .thenReturn(mock(StateFlow.class));
mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
mLockscreenUserManager.setUpWithPresenter(mPresenter);
@@ -504,11 +516,85 @@
}
@Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedIfNotLocked() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ // Claim the device was last locked 1 day ago
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
+ // Device is not currently locked
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(false);
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedIfUnlockedSinceReceipt() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ // Device was locked after this notification arrived
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1));
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedIfNotLockedForLongEnough() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ // Device has been locked for 1 second before the notification came in, which is too short
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1));
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @DisableFlags(LockscreenOtpRedaction.FLAG_NAME)
+ public void testHasSensitiveContent_notRedactedFlagDisabled() {
+ // Allow private notifications for this user
+ mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+ mCurrentUser.id);
+ changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ // Claim the device was last locked 1 day ago
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+
+ // Sensitive Content notifications are always redacted
+ assertEquals(REDACTION_TYPE_NONE,
+ mLockscreenUserManager.getRedactionType(mSensitiveContentNotif));
+ }
+
+ @Test
+ @EnableFlags(LockscreenOtpRedaction.FLAG_NAME)
public void testHasSensitiveContent_redacted() {
// Allow private notifications for this user
mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
mCurrentUser.id);
changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ when(mKeyguardManager.isDeviceLocked()).thenReturn(true);
+ // Claim the device was last unlocked 1 day ago
+ mLockscreenUserManager.mLastLockTime
+ .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1));
// Sensitive Content notifications are always redacted
assertEquals(REDACTION_TYPE_SENSITIVE_CONTENT,
@@ -1066,7 +1152,9 @@
mock(DumpManager.class),
mock(LockPatternUtils.class),
mFakeFeatureFlags,
- mDeviceUnlockedInteractorLazy
+ mDeviceUnlockedInteractorLazy,
+ mKeyguardInteractorLazy,
+ null //CoroutineScope
);
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index a62d9d5..0061c41 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -132,7 +132,7 @@
val latest by collectLastValue(underTest.chip)
repo.setOngoingCallState(
- inCallModel(startTimeMs = 1000, notificationIcon = mock<StatusBarIconView>())
+ inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull())
)
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -147,11 +147,12 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon))
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -165,6 +166,24 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ repo.setOngoingCallState(
+ inCallModel(
+ startTimeMs = 1000,
+ notificationIcon = createStatusBarIconViewOrNull(),
+ notificationKey = "notifKey",
+ )
+ )
+
+ assertThat((latest as OngoingActivityChipModel.Shown).icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+ }
+
+ @Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -192,7 +211,7 @@
val latest by collectLastValue(underTest.chip)
repo.setOngoingCallState(
- inCallModel(startTimeMs = 0, notificationIcon = mock<StatusBarIconView>())
+ inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull())
)
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -207,11 +226,12 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun chip_zeroStartTime_notifIconFlagOn_iconIsNotifIcon() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
repo.setOngoingCallState(inCallModel(startTimeMs = 0, notificationIcon = notifIcon))
assertThat((latest as OngoingActivityChipModel.Shown).icon)
@@ -224,8 +244,27 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ repo.setOngoingCallState(
+ inCallModel(
+ startTimeMs = 0,
+ notificationIcon = createStatusBarIconViewOrNull(),
+ notificationKey = "notifKey",
+ )
+ )
+
+ assertThat((latest as OngoingActivityChipModel.Shown).icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+ }
+
+ @Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun chip_notifIconFlagOn_butNullNotifIcon_iconIsPhone() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -242,6 +281,24 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ repo.setOngoingCallState(
+ inCallModel(
+ startTimeMs = 1000,
+ notificationIcon = null,
+ notificationKey = "notifKey",
+ )
+ )
+
+ assertThat((latest as OngoingActivityChipModel.Shown).icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey"))
+ }
+
+ @Test
fun chip_positiveStartTime_colorsAreThemed() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
@@ -330,4 +387,13 @@
verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
}
+
+ companion object {
+ fun createStatusBarIconViewOrNull(): StatusBarIconView? =
+ if (StatusBarConnectedDisplays.isEnabled) {
+ null
+ } else {
+ mock<StatusBarIconView>()
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
index 0d033a4..fe15eac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.notification.domain.interactor
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -148,7 +149,8 @@
}
@Test
- fun notificationChip_missingStatusBarIconChipView_inConstructor_emitsNull() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_missingStatusBarIconChipView_cdFlagDisabled_inConstructor_emitsNull() =
kosmos.runTest {
val underTest =
factory.create(
@@ -167,6 +169,25 @@
@Test
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_missingStatusBarIconChipView_cdFlagEnabled_inConstructor_emitsNotNull() =
+ kosmos.runTest {
+ val underTest =
+ factory.create(
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = null,
+ promotedContent = PROMOTED_CONTENT,
+ ),
+ 32L,
+ )
+
+ val latest by collectLastValue(underTest.notificationChip)
+
+ assertThat(latest).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun notificationChip_cdEnabled_missingStatusBarIconChipView_inConstructor_emitsNotNull() =
kosmos.runTest {
val underTest =
@@ -186,7 +207,8 @@
}
@Test
- fun notificationChip_missingStatusBarIconChipView_inSet_emitsNull() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_cdFlagDisabled_missingStatusBarIconChipView_inSet_emitsNull() =
kosmos.runTest {
val startingNotif =
activeNotificationModel(
@@ -211,6 +233,31 @@
@Test
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChip_cdFlagEnabled_missingStatusBarIconChipView_inSet_emitsNotNull() =
+ kosmos.runTest {
+ val startingNotif =
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = mock(),
+ promotedContent = PROMOTED_CONTENT,
+ )
+ val underTest = factory.create(startingNotif, 123L)
+ val latest by collectLastValue(underTest.notificationChip)
+ assertThat(latest).isNotNull()
+
+ underTest.setNotification(
+ activeNotificationModel(
+ key = "notif1",
+ statusBarChipIcon = null,
+ promotedContent = PROMOTED_CONTENT,
+ )
+ )
+
+ assertThat(latest).isNotNull()
+ }
+
+ @Test
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
fun notificationChip_missingStatusBarIconChipView_inSet_cdEnabled_emitsNotNull() =
kosmos.runTest {
val startingNotif =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
index f703d78..ee4a52d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt
@@ -30,6 +30,7 @@
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -83,7 +84,8 @@
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
- fun notificationChips_notifMissingStatusBarChipIconView_empty() =
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() =
kosmos.runTest {
val latest by collectLastValue(underTest.notificationChips)
@@ -101,6 +103,25 @@
}
@Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarConnectedDisplays.FLAG_NAME)
+ fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.notificationChips)
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = null,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ )
+ )
+ )
+
+ assertThat(latest).isNotEmpty()
+ }
+
+ @Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
fun notificationChips_onePromotedNotif_statusBarIconViewMatches() =
kosmos.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
index 17076b4..e561e3e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt
@@ -23,7 +23,6 @@
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.kosmos.collectLastValue
@@ -31,6 +30,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
@@ -48,7 +48,6 @@
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
@@ -84,8 +83,8 @@
}
@Test
- @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
- fun chips_notifMissingStatusBarChipIconView_empty() =
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY, StatusBarConnectedDisplays.FLAG_NAME)
+ fun chips_notifMissingStatusBarChipIconView_cdFlagDisabled_empty() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
@@ -104,11 +103,31 @@
@Test
@DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
+ @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun chips_notifMissingStatusBarChipIconView_cdFlagEnabled_notEmpty() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chips)
+
+ setNotifs(
+ listOf(
+ activeNotificationModel(
+ key = "notif",
+ statusBarChipIcon = null,
+ promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
+ )
+ )
+ )
+
+ assertThat(latest).isNotEmpty()
+ }
+
+ @Test
+ @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY)
fun chips_onePromotedNotif_statusBarIconViewMatches() =
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
- val icon = mock<StatusBarIconView>()
+ val icon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -121,8 +140,7 @@
assertThat(latest).hasSize(1)
val chip = latest!![0]
- assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
- assertThat(chip.icon).isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(icon))
+ assertIsNotifChip(chip, icon, "notif")
}
@Test
@@ -168,7 +186,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -187,8 +205,8 @@
kosmos.runTest {
val latest by collectLastValue(underTest.chips)
- val firstIcon = mock<StatusBarIconView>()
- val secondIcon = mock<StatusBarIconView>()
+ val firstIcon = createStatusBarIconViewOrNull()
+ val secondIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -203,15 +221,15 @@
),
activeNotificationModel(
key = "notif3",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = null,
),
)
)
assertThat(latest).hasSize(2)
- assertIsNotifChip(latest!![0], firstIcon)
- assertIsNotifChip(latest!![1], secondIcon)
+ assertIsNotifChip(latest!![0], firstIcon, "notif1")
+ assertIsNotifChip(latest!![1], secondIcon, "notif2")
}
@Test
@@ -269,7 +287,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -293,7 +311,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -323,7 +341,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -353,7 +371,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -382,7 +400,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -411,7 +429,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -439,7 +457,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -467,7 +485,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -499,7 +517,7 @@
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = promotedContentBuilder.build(),
)
)
@@ -531,7 +549,7 @@
listOf(
activeNotificationModel(
key = "clickTest",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent =
PromotedNotificationContentModel.Builder("clickTest").build(),
)
@@ -552,9 +570,21 @@
}
companion object {
- fun assertIsNotifChip(latest: OngoingActivityChipModel?, expectedIcon: StatusBarIconView) {
- assertThat((latest as OngoingActivityChipModel.Shown).icon)
- .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon))
+ fun assertIsNotifChip(
+ latest: OngoingActivityChipModel?,
+ expectedIcon: StatusBarIconView?,
+ notificationKey: String,
+ ) {
+ val shown = latest as OngoingActivityChipModel.Shown
+ if (StatusBarConnectedDisplays.isEnabled) {
+ assertThat(shown.icon)
+ .isEqualTo(
+ OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon(notificationKey)
+ )
+ } else {
+ assertThat(latest.icon)
+ .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarView(expectedIcon!!))
+ }
}
fun assertIsNotifKey(latest: OngoingActivityChipModel?, expectedKey: String) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 4fb42e9..42358cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -41,6 +41,7 @@
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
@@ -169,29 +170,35 @@
@Test
fun primaryChip_screenRecordAndShareToAppAndCastToOtherHideAndCallShown_callShown() =
testScope.runTest {
+ val notificationKey = "call"
screenRecordState.value = ScreenRecordModel.DoingNothing
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = notificationKey)
+ )
val latest by collectLastValue(underTest.primaryChip)
- assertIsCallChip(latest)
+ assertIsCallChip(latest, notificationKey)
}
@Test
fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
testScope.runTest {
// Start with just the lowest priority chip shown
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
// And everything else hidden
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
val latest by collectLastValue(underTest.primaryChip)
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -218,7 +225,10 @@
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.primaryChip)
@@ -235,7 +245,7 @@
mediaProjectionState.value = MediaProjectionState.NotProjecting
// THEN the lower priority call is used
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
}
/** Regression test for b/347726238. */
@@ -364,13 +374,27 @@
assertThat(icon.res).isEqualTo(R.drawable.ic_present_to_all)
}
- fun assertIsCallChip(latest: OngoingActivityChipModel?) {
- assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+ fun assertIsCallChip(latest: OngoingActivityChipModel?, notificationKey: String) {
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
+ if (StatusBarConnectedDisplays.isEnabled) {
+ assertNotificationIcon(latest, notificationKey)
+ return
+ }
val icon =
(((latest as OngoingActivityChipModel.Shown).icon)
as OngoingActivityChipModel.ChipIcon.SingleColorIcon)
.impl as Icon.Resource
assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone)
}
+
+ private fun assertNotificationIcon(
+ latest: OngoingActivityChipModel?,
+ notificationKey: String,
+ ) {
+ val shown = latest as OngoingActivityChipModel.Shown
+ val notificationIcon =
+ shown.icon as OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+ assertThat(notificationIcon.notificationKey).isEqualTo(notificationKey)
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 0050ebe..0f42f29 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -34,7 +34,7 @@
import com.android.systemui.res.R
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
-import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModelTest.Companion.createStatusBarIconViewOrNull
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor
@@ -186,13 +186,16 @@
@Test
fun chips_screenRecordShowAndCallShow_primaryIsScreenRecordSecondaryIsCall() =
testScope.runTest {
+ val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.chips)
assertIsScreenRecordChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
}
@Test
@@ -240,15 +243,18 @@
@Test
fun chips_shareToAppShowAndCallShow_primaryIsShareToAppSecondaryIsCall() =
testScope.runTest {
+ val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.chips)
assertIsShareToAppChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
}
@Test
@@ -258,25 +264,31 @@
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.primaryChip)
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
}
@Test
fun chips_onlyCallShown_primaryIsCallSecondaryIsHidden() =
testScope.runTest {
+ val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.DoingNothing
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
val latest by collectLastValue(underTest.chips)
- assertIsCallChip(latest!!.primary)
+ assertIsCallChip(latest!!.primary, callNotificationKey)
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@@ -285,7 +297,7 @@
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- val icon = mock<StatusBarIconView>()
+ val icon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -296,7 +308,7 @@
)
)
- assertIsNotifChip(latest!!.primary, icon)
+ assertIsNotifChip(latest!!.primary, icon, "notif")
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
@@ -305,8 +317,8 @@
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- val firstIcon = mock<StatusBarIconView>()
- val secondIcon = mock<StatusBarIconView>()
+ val firstIcon = createStatusBarIconViewOrNull()
+ val secondIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -324,8 +336,8 @@
)
)
- assertIsNotifChip(latest!!.primary, firstIcon)
- assertIsNotifChip(latest!!.secondary, secondIcon)
+ assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif")
+ assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif")
}
@Test
@@ -333,9 +345,9 @@
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- val firstIcon = mock<StatusBarIconView>()
- val secondIcon = mock<StatusBarIconView>()
- val thirdIcon = mock<StatusBarIconView>()
+ val firstIcon = createStatusBarIconViewOrNull()
+ val secondIcon = createStatusBarIconViewOrNull()
+ val thirdIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -359,8 +371,8 @@
)
)
- assertIsNotifChip(latest!!.primary, firstIcon)
- assertIsNotifChip(latest!!.secondary, secondIcon)
+ assertIsNotifChip(latest!!.primary, firstIcon, "firstNotif")
+ assertIsNotifChip(latest!!.secondary, secondIcon, "secondNotif")
}
@Test
@@ -368,8 +380,12 @@
testScope.runTest {
val latest by collectLastValue(underTest.chips)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- val firstIcon = mock<StatusBarIconView>()
+ val callNotificationKey = "call"
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
+
+ val firstIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -380,43 +396,47 @@
),
activeNotificationModel(
key = "secondNotif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent =
PromotedNotificationContentModel.Builder("secondNotif").build(),
),
)
)
- assertIsCallChip(latest!!.primary)
- assertIsNotifChip(latest!!.secondary, firstIcon)
+ assertIsCallChip(latest!!.primary, callNotificationKey)
+ assertIsNotifChip(latest!!.secondary, firstIcon, "firstNotif")
}
@Test
fun chips_screenRecordAndCallAndPromotedNotifs_notifsNotShown() =
testScope.runTest {
+ val callNotificationKey = "call"
val latest by collectLastValue(underTest.chips)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
screenRecordState.value = ScreenRecordModel.Recording
setNotifs(
listOf(
activeNotificationModel(
key = "notif",
- statusBarChipIcon = mock<StatusBarIconView>(),
+ statusBarChipIcon = createStatusBarIconViewOrNull(),
promotedContent = PromotedNotificationContentModel.Builder("notif").build(),
)
)
)
assertIsScreenRecordChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
}
@Test
fun primaryChip_higherPriorityChipAdded_lowerPriorityChipReplaced() =
testScope.runTest {
+ val callNotificationKey = "call"
// Start with just the lowest priority chip shown
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -433,13 +453,15 @@
val latest by collectLastValue(underTest.primaryChip)
- assertIsNotifChip(latest, notifIcon)
+ assertIsNotifChip(latest, notifIcon, "notif")
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
// THEN the higher priority call chip is used
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -462,12 +484,15 @@
@Test
fun primaryChip_highestPriorityChipRemoved_showsNextPriorityChip() =
testScope.runTest {
+ val callNotificationKey = "call"
// WHEN all chips are active
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
- val notifIcon = mock<StatusBarIconView>()
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
+ val notifIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -493,20 +518,21 @@
mediaProjectionState.value = MediaProjectionState.NotProjecting
// THEN the lower priority call is used
- assertIsCallChip(latest)
+ assertIsCallChip(latest, callNotificationKey)
// WHEN the higher priority call is removed
callRepo.setOngoingCallState(OngoingCallModel.NoCall)
// THEN the lower priority notif is used
- assertIsNotifChip(latest, notifIcon)
+ assertIsNotifChip(latest, notifIcon, "notif")
}
@Test
fun chips_movesChipsAroundAccordingToPriority() =
testScope.runTest {
+ val callNotificationKey = "call"
// Start with just the lowest priority chip shown
- val notifIcon = mock<StatusBarIconView>()
+ val notifIcon = createStatusBarIconViewOrNull()
setNotifs(
listOf(
activeNotificationModel(
@@ -523,16 +549,18 @@
val latest by collectLastValue(underTest.chips)
- assertIsNotifChip(latest!!.primary, notifIcon)
+ assertIsNotifChip(latest!!.primary, notifIcon, "notif")
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
// WHEN the higher priority call chip is added
- callRepo.setOngoingCallState(inCallModel(startTimeMs = 34))
+ callRepo.setOngoingCallState(
+ inCallModel(startTimeMs = 34, notificationKey = callNotificationKey)
+ )
// THEN the higher priority call chip is used as primary and notif is demoted to
// secondary
- assertIsCallChip(latest!!.primary)
- assertIsNotifChip(latest!!.secondary, notifIcon)
+ assertIsCallChip(latest!!.primary, callNotificationKey)
+ assertIsNotifChip(latest!!.secondary, notifIcon, "notif")
// WHEN the higher priority media projection chip is added
mediaProjectionState.value =
@@ -545,7 +573,7 @@
// THEN the higher priority media projection chip is used as primary and call is demoted
// to secondary (and notif is dropped altogether)
assertIsShareToAppChip(latest!!.primary)
- assertIsCallChip(latest!!.secondary)
+ assertIsCallChip(latest!!.secondary, callNotificationKey)
// WHEN the higher priority screen record chip is added
screenRecordState.value = ScreenRecordModel.Recording
@@ -559,13 +587,13 @@
// THEN media projection and notif remain
assertIsShareToAppChip(latest!!.primary)
- assertIsNotifChip(latest!!.secondary, notifIcon)
+ assertIsNotifChip(latest!!.secondary, notifIcon, "notif")
// WHEN media projection is dropped
mediaProjectionState.value = MediaProjectionState.NotProjecting
// THEN notif is promoted to primary
- assertIsNotifChip(latest!!.primary, notifIcon)
+ assertIsNotifChip(latest!!.primary, notifIcon, "notif")
assertThat(latest!!.secondary).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
index feda0c6..ab475c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
@@ -32,10 +32,10 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.phone.BoundsPair
-import com.android.systemui.statusbar.phone.LetterboxAppearance
-import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.BoundsPair
+import com.android.systemui.statusbar.layout.LetterboxAppearance
+import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
index 4795a12..db51a58 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt
@@ -33,7 +33,7 @@
import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 5f3668a..0a96013 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -27,8 +27,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.AnimatorTestRule
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
similarity index 82%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
index 518b327..f1affbc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculatorTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.graphics.Color
import android.graphics.Rect
-import android.view.WindowInsetsController
+import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -42,7 +42,7 @@
companion object {
private const val DEFAULT_APPEARANCE = 0
- private const val TEST_APPEARANCE = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+ private const val TEST_APPEARANCE = APPEARANCE_LIGHT_STATUS_BARS
private val TEST_APPEARANCE_REGION_BOUNDS = Rect(0, 0, 20, 100)
private val TEST_APPEARANCE_REGION =
AppearanceRegion(TEST_APPEARANCE, TEST_APPEARANCE_REGION_BOUNDS)
@@ -74,7 +74,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
expect
.that(letterboxAppearance.appearance)
@@ -90,7 +94,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
expect
.that(letterboxAppearance.appearance)
@@ -109,10 +117,10 @@
val letterBoxInnerBoundsCopy = Rect(letterBoxInnerBounds)
calculator.getLetterboxAppearance(
- TEST_APPEARANCE,
- TEST_APPEARANCE_REGIONS,
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
listOf(letterboxWithInnerBounds(letterBoxInnerBounds)),
- BoundsPair(statusBarStartSideBounds, statusBarEndSideBounds)
+ BoundsPair(statusBarStartSideBounds, statusBarEndSideBounds),
)
expect.that(statusBarStartSideBounds).isEqualTo(statusBarStartSideBoundsCopy)
@@ -129,11 +137,15 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
expect
- .that(letterboxAppearance.appearance)
- .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
+ .that(letterboxAppearance.appearance)
+ .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
}
@@ -145,7 +157,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -158,7 +174,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -171,7 +191,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -184,7 +208,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ TEST_APPEARANCE_REGIONS,
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@@ -198,7 +226,11 @@
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, listOf(letterboxRegion), listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ listOf(letterboxRegion),
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
val letterboxAdaptedRegion = letterboxRegion.copy(bounds = letterbox.letterboxInnerBounds)
assertThat(letterboxAppearance.appearanceRegions.toList()).contains(letterboxAdaptedRegion)
@@ -212,12 +244,17 @@
val letterbox =
letterboxWithBounds(
innerBounds = Rect(left = 25, top = 0, right = 75, bottom = 100),
- fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100))
+ fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100),
+ )
val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
val letterboxAppearance =
calculator.getLetterboxAppearance(
- TEST_APPEARANCE, listOf(letterboxRegion), listOf(letterbox), BoundsPair(start, end))
+ TEST_APPEARANCE,
+ listOf(letterboxRegion),
+ listOf(letterbox),
+ BoundsPair(start, end),
+ )
val outerRegions =
listOf(
@@ -230,8 +267,7 @@
Rect(left = 75, top = 0, right = 100, bottom = 100),
),
)
- assertThat(letterboxAppearance.appearanceRegions)
- .containsAtLeastElementsIn(outerRegions)
+ assertThat(letterboxAppearance.appearanceRegions).containsAtLeastElementsIn(outerRegions)
}
private fun letterboxWithBounds(innerBounds: Rect, fullBounds: Rect) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
index b9cfe21..04319f05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarBoundsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarBoundsProviderTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.graphics.Rect
import android.testing.TestableLooper.RunWithLooper
@@ -23,7 +23,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider.BoundsChangeListener
import com.android.systemui.util.mockito.any
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -47,7 +46,7 @@
private val END_SIDE_BOUNDS = Rect(250, 300, 350, 400)
}
- @Mock private lateinit var boundsChangeListener: BoundsChangeListener
+ @Mock private lateinit var boundsChangeListener: StatusBarBoundsProvider.BoundsChangeListener
private lateinit var boundsProvider: StatusBarBoundsProvider
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
similarity index 99%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
index 7a51b2d..c9c9617 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.content.Context
import android.content.res.Configuration
@@ -32,6 +32,7 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.leak.RotationUtils
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
index 92318b9..ffd349d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt
@@ -53,6 +53,7 @@
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.ui.StatusBarIconController
import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.policy.BatteryController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
index 788c2cb2..7786689 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
@@ -25,6 +25,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.layout.LetterboxBackgroundProvider
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
@@ -134,6 +135,7 @@
fun isLetterboxBackgroundMultiColored_defaultValue_returnsFalse() {
assertThat(provider.isLetterboxBackgroundMultiColored).isEqualTo(false)
}
+
@Test
fun isLetterboxBackgroundMultiColored_afterOnStart_executorNotDone_returnsDefaultValue() {
whenever(windowManager.isLetterboxBackgroundMultiColored).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index 9099334..a65ccad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -51,6 +51,7 @@
import com.android.systemui.statusbar.data.model.StatusBarAppearance;
import com.android.systemui.statusbar.data.model.StatusBarMode;
import com.android.systemui.statusbar.data.repository.FakeStatusBarModePerDisplayRepository;
+import com.android.systemui.statusbar.layout.BoundsPair;
import com.android.systemui.statusbar.policy.BatteryController;
import kotlinx.coroutines.test.TestScope;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index d174484..2e12336 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -40,7 +40,6 @@
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
-import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.TestableLooper;
@@ -610,7 +609,6 @@
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_registration() {
/* verify that a predictive back callback is registered when the bouncer becomes visible */
mBouncerExpansionCallback.onVisibilityChanged(true);
@@ -625,7 +623,6 @@
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_invocationHidesBouncer() {
mBouncerExpansionCallback.onVisibilityChanged(true);
/* capture the predictive back callback during registration */
@@ -643,7 +640,6 @@
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_noBackAnimationForFullScreenBouncer() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
.thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
@@ -663,7 +659,6 @@
}
@Test
- @RequiresFlagsEnabled(com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_ANIMATE_BOUNCER)
public void testPredictiveBackCallback_forwardsBackDispatches() {
mBouncerExpansionCallback.onVisibilityChanged(true);
/* capture the predictive back callback during registration */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 0652a83..650fa7c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -31,7 +31,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
-import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.ravenwood.RavenwoodRule;
@@ -41,7 +40,6 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.Dependency;
-import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.animation.back.BackAnimationSpec;
@@ -137,7 +135,6 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_PREDICTIVE_BACK_ANIMATE_DIALOGS)
public void usePredictiveBackAnimFlag() {
final SystemUIDialog dialog = new SystemUIDialog(mContext);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 3d6882c..c6bae19 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -18,9 +18,8 @@
import android.app.AutomaticZenRule
import android.app.Flags
-import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
-import android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY
import android.app.NotificationManager.Policy
+import android.media.AudioManager
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION
@@ -34,6 +33,7 @@
import com.android.internal.R
import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -402,47 +402,12 @@
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
- fun activeModesBlockingEverything_hasModesWithFilterNone() =
- testScope.runTest {
- val blockingEverything by collectLastValue(underTest.activeModesBlockingEverything)
-
- zenModeRepository.addModes(
- listOf(
- TestModeBuilder()
- .setName("Filter=None, Not active")
- .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
- .setActive(false)
- .build(),
- TestModeBuilder()
- .setName("Filter=Priority, Active")
- .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
- .setActive(true)
- .build(),
- TestModeBuilder()
- .setName("Filter=None, Active")
- .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
- .setActive(true)
- .build(),
- TestModeBuilder()
- .setName("Filter=None, Active Too")
- .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
- .setActive(true)
- .build(),
- )
- )
- runCurrent()
-
- assertThat(blockingEverything!!.mainMode!!.name).isEqualTo("Filter=None, Active")
- assertThat(blockingEverything!!.modeNames)
- .containsExactly("Filter=None, Active", "Filter=None, Active Too")
- .inOrder()
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MODES_UI)
fun activeModesBlockingMedia_hasModesWithPolicyBlockingMedia() =
testScope.runTest {
- val blockingMedia by collectLastValue(underTest.activeModesBlockingMedia)
+ val blockingMedia by
+ collectLastValue(
+ underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_MUSIC))
+ )
zenModeRepository.addModes(
listOf(
@@ -480,7 +445,10 @@
@EnableFlags(Flags.FLAG_MODES_UI)
fun activeModesBlockingAlarms_hasModesWithPolicyBlockingAlarms() =
testScope.runTest {
- val blockingAlarms by collectLastValue(underTest.activeModesBlockingAlarms)
+ val blockingAlarms by
+ collectLastValue(
+ underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_ALARM))
+ )
zenModeRepository.addModes(
listOf(
@@ -515,6 +483,47 @@
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_UI)
+ fun activeModesBlockingAlarms_hasModesWithPolicyBlockingSystem() =
+ testScope.runTest {
+ val blockingSystem by
+ collectLastValue(
+ underTest.activeModesBlockingStream(AudioStream(AudioManager.STREAM_SYSTEM))
+ )
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Blocks system, Not active")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
+ .setActive(false)
+ .build(),
+ TestModeBuilder()
+ .setName("Allows system, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(true).build())
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Blocks system, Active")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
+ .setActive(true)
+ .build(),
+ TestModeBuilder()
+ .setName("Blocks system, Active Too")
+ .setZenPolicy(ZenPolicy.Builder().allowSystem(false).build())
+ .setActive(true)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(blockingSystem!!.mainMode!!.name).isEqualTo("Blocks system, Active")
+ assertThat(blockingSystem!!.modeNames)
+ .containsExactly("Blocks system, Active", "Blocks system, Active Too")
+ .inOrder()
+ }
+
+ @Test
@EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
index d3071f8..51cac69 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt
@@ -23,66 +23,40 @@
import android.service.notification.ZenPolicy
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.internal.logging.uiEventLogger
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
-import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import com.android.systemui.testKosmos
-import com.android.systemui.volume.domain.interactor.audioVolumeInteractor
-import com.android.systemui.volume.shared.volumePanelLogger
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class AudioStreamSliderViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
private val zenModeRepository = kosmos.fakeZenModeRepository
- private lateinit var mediaStream: AudioStreamSliderViewModel
- private lateinit var alarmsStream: AudioStreamSliderViewModel
- private lateinit var notificationStream: AudioStreamSliderViewModel
- private lateinit var otherStream: AudioStreamSliderViewModel
-
- @Before
- fun setUp() {
- mediaStream = audioStreamSliderViewModel(AudioManager.STREAM_MUSIC)
- alarmsStream = audioStreamSliderViewModel(AudioManager.STREAM_ALARM)
- notificationStream = audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION)
- otherStream = audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL)
- }
-
- private fun audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
- return AudioStreamSliderViewModel(
+ private fun Kosmos.audioStreamSliderViewModel(stream: Int): AudioStreamSliderViewModel {
+ return audioStreamSliderViewModelFactory.create(
AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
- testScope.backgroundScope,
- context,
- kosmos.audioVolumeInteractor,
- kosmos.zenModeInteractor,
- kosmos.uiEventLogger,
- kosmos.volumePanelLogger,
- kosmos.sliderHapticsViewModelFactory,
+ applicationCoroutineScope,
)
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
fun slider_media_hasDisabledByModesText() =
- testScope.runTest {
- val mediaSlider by collectLastValue(mediaStream.slider)
+ kosmos.runTest {
+ val mediaSlider by
+ collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_MUSIC).slider)
zenModeRepository.addMode(
TestModeBuilder()
@@ -112,8 +86,9 @@
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
fun slider_alarms_hasDisabledByModesText() =
- testScope.runTest {
- val alarmsSlider by collectLastValue(alarmsStream.slider)
+ kosmos.runTest {
+ val alarmsSlider by
+ collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_ALARM).slider)
zenModeRepository.addMode(
TestModeBuilder()
@@ -141,9 +116,10 @@
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
- fun slider_other_hasDisabledByModesText() =
- testScope.runTest {
- val otherSlider by collectLastValue(otherStream.slider)
+ fun slider_other_hasDisabledText() =
+ kosmos.runTest {
+ val otherSlider by
+ collectLastValue(audioStreamSliderViewModel(AudioManager.STREAM_VOICE_CALL).slider)
zenModeRepository.addMode(
TestModeBuilder()
@@ -154,20 +130,17 @@
)
runCurrent()
- assertThat(otherSlider!!.disabledMessage)
- .isEqualTo("Unavailable because Everything blocked is on")
-
- zenModeRepository.clearModes()
- runCurrent()
-
assertThat(otherSlider!!.disabledMessage).isEqualTo("Unavailable")
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS)
fun slider_notification_hasSpecialDisabledText() =
- testScope.runTest {
- val notificationSlider by collectLastValue(notificationStream.slider)
+ kosmos.runTest {
+ val notificationSlider by
+ collectLastValue(
+ audioStreamSliderViewModel(AudioManager.STREAM_NOTIFICATION).slider
+ )
runCurrent()
assertThat(notificationSlider!!.disabledMessage)
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 7d220b5..6e23a07 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -21,11 +21,9 @@
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
-import android.window.TaskSnapshot;
import android.window.WindowAnimationState;
import com.android.internal.os.IResultReceiver;
-import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.wm.shell.recents.IRecentsAnimationController;
public class RecentsAnimationControllerCompat {
@@ -40,18 +38,6 @@
mAnimationController = animationController;
}
- public ThumbnailData screenshotTask(int taskId) {
- try {
- final TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
- if (snapshot != null) {
- return ThumbnailData.fromSnapshot(snapshot);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to screenshot task", e);
- }
- return new ThumbnailData();
- }
-
public void setInputConsumerEnabled(boolean enabled) {
try {
mAnimationController.setInputConsumerEnabled(enabled);
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 2978595..9596a54 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -20,10 +20,12 @@
import android.hardware.input.InputManager
import android.hardware.input.KeyGestureEvent
import androidx.datastore.core.DataStore
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
@@ -68,7 +70,7 @@
suspend fun updateGestureEduModel(
gestureType: GestureType,
- transform: (GestureEduModel) -> GestureEduModel
+ transform: (GestureEduModel) -> GestureEduModel,
)
suspend fun updateEduDeviceConnectionTime(
@@ -149,6 +151,8 @@
String.format(DATASTORE_DIR, userId)
)
},
+ corruptionHandler =
+ ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
scope = newDsScope,
)
dataStoreScope = newDsScope
@@ -159,7 +163,7 @@
private fun getGestureEduModel(
gestureType: GestureType,
- preferences: Preferences
+ preferences: Preferences,
): GestureEduModel {
return GestureEduModel(
signalCount = preferences[getSignalCountKey(gestureType)] ?: 0,
@@ -183,7 +187,7 @@
override suspend fun updateGestureEduModel(
gestureType: GestureType,
- transform: (GestureEduModel) -> GestureEduModel
+ transform: (GestureEduModel) -> GestureEduModel,
) {
datastore.filterNotNull().first().edit { preferences ->
val currentModel = getGestureEduModel(gestureType, preferences)
@@ -193,17 +197,17 @@
setInstant(
preferences,
updatedModel.lastShortcutTriggeredTime,
- getLastShortcutTriggeredTimeKey(gestureType)
+ getLastShortcutTriggeredTimeKey(gestureType),
)
setInstant(
preferences,
updatedModel.usageSessionStartTime,
- getUsageSessionStartTimeKey(gestureType)
+ getUsageSessionStartTimeKey(gestureType),
)
setInstant(
preferences,
updatedModel.lastEducationTime,
- getLastEducationTimeKey(gestureType)
+ getLastEducationTimeKey(gestureType),
)
}
}
@@ -220,12 +224,12 @@
setInstant(
preferences,
updatedModel.keyboardFirstConnectionTime,
- getKeyboardFirstConnectionTimeKey()
+ getKeyboardFirstConnectionTimeKey(),
)
setInstant(
preferences,
updatedModel.touchpadFirstConnectionTime,
- getTouchpadFirstConnectionTimeKey()
+ getTouchpadFirstConnectionTimeKey(),
)
}
}
@@ -235,7 +239,7 @@
keyboardFirstConnectionTime =
preferences[getKeyboardFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
touchpadFirstConnectionTime =
- preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) }
+ preferences[getTouchpadFirstConnectionTimeKey()]?.let { Instant.ofEpochSecond(it) },
)
}
@@ -263,7 +267,7 @@
private fun setInstant(
preferences: MutablePreferences,
instant: Instant?,
- key: Preferences.Key<Long>
+ key: Preferences.Key<Long>,
) {
if (instant != null) {
// Use epochSecond because an instant is defined as a signed long (64bit number) of
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
index 19a19d5..c702ba9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperCoreStartable.kt
@@ -25,6 +25,7 @@
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.CommandQueue
@@ -41,6 +42,7 @@
private val stateRepository: ShortcutHelperStateRepository,
private val activityStarter: ActivityStarter,
@Background private val backgroundScope: CoroutineScope,
+ private val customInputGesturesRepository: CustomInputGesturesRepository
) : CoreStartable {
override fun start() {
registerBroadcastReceiver(
@@ -55,6 +57,10 @@
action = Intent.ACTION_CLOSE_SYSTEM_DIALOGS,
onReceive = { stateRepository.hide() },
)
+ registerBroadcastReceiver(
+ action = Intent.ACTION_USER_SWITCHED,
+ onReceive = { customInputGesturesRepository.refreshCustomInputGestures() },
+ )
commandQueue.addCallback(
object : CommandQueue.Callbacks {
override fun dismissKeyboardShortcutsMenu() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
index 36cd400..e5c638c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomInputGesturesRepository.kt
@@ -25,6 +25,7 @@
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.InputSettings
import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult.ERROR_OTHER
@@ -37,6 +38,7 @@
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
+@SysUISingleton
class CustomInputGesturesRepository
@Inject
constructor(private val userTracker: UserTracker,
@@ -56,7 +58,7 @@
val customInputGestures =
_customInputGesture.onStart { refreshCustomInputGestures() }
- private fun refreshCustomInputGestures() {
+ fun refreshCustomInputGestures() {
setCustomInputGestures(inputGestures = retrieveCustomInputGestures())
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
index 5b28a3f..a74384f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WindowManagerLockscreenVisibilityManager.kt
@@ -156,6 +156,13 @@
setWmLockscreenState(lockscreenShowing = lockscreenShown)
}
+ /**
+ * Called when the keyguard going away remote animation is started, and we have a
+ * RemoteAnimationTarget to animate.
+ *
+ * This is triggered either by this class calling ATMS#keyguardGoingAway, or by WM directly,
+ * such as when an activity with FLAG_DISMISS_KEYGUARD is launched over a dismissible keyguard.
+ */
fun onKeyguardGoingAwayRemoteAnimationStart(
@WindowManager.TransitionOldType transit: Int,
apps: Array<RemoteAnimationTarget>,
@@ -163,19 +170,32 @@
nonApps: Array<RemoteAnimationTarget>,
finishedCallback: IRemoteAnimationFinishedCallback,
) {
- // Make sure this is true - we set it true when requesting keyguardGoingAway, but there are
- // cases where WM starts this transition on its own.
- isKeyguardGoingAway = true
+ // If we weren't expecting the keyguard to be going away, WM triggered this transition.
+ if (!isKeyguardGoingAway) {
+ // Since WM triggered this, we're likely not transitioning to GONE yet. See if we can
+ // start that transition.
+ val startedDismiss =
+ keyguardDismissTransitionInteractor.startDismissKeyguardTransition(
+ reason = "Going away remote animation started"
+ )
- // Ensure that we've started a dismiss keyguard transition. WindowManager can start the
- // going away animation on its own, if an activity launches and then requests dismissing the
- // keyguard. In this case, this is the first and only signal we'll receive to start
- // a transition to GONE. This transition needs to start even if we're not provided an app
- // animation target - it's possible the app is destroyed on creation, etc. but we'll still
- // be unlocking.
- keyguardDismissTransitionInteractor.startDismissKeyguardTransition(
- reason = "Going away remote animation started"
- )
+ if (!startedDismiss) {
+ // If the transition wasn't started, we're already GONE. This can happen with timing
+ // issues, where the remote animation took a long time to start, and something else
+ // caused us to unlock in the meantime. Since we're already GONE, simply end the
+ // remote animatiom immediately.
+ Log.d(
+ TAG,
+ "onKeyguardGoingAwayRemoteAnimationStart: " +
+ "Dismiss transition was not started; we're already GONE. " +
+ "Ending remote animation.",
+ )
+ finishedCallback.onAnimationFinished()
+ return
+ }
+
+ isKeyguardGoingAway = true
+ }
if (apps.isNotEmpty()) {
goingAwayRemoteAnimationFinishedCallback = finishedCallback
@@ -278,6 +298,6 @@
}
companion object {
- private val TAG = WindowManagerLockscreenVisibilityManager::class.java.simpleName
+ private val TAG = "WindowManagerLsVis"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
index 4793d95..089e5dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissTransitionInteractor.kt
@@ -48,9 +48,12 @@
*
* This is called exclusively by sources that can authoritatively say we should be unlocked,
* including KeyguardSecurityContainerController and WindowManager.
+ *
+ * Returns [false] if the transition was not started, because we're already GONE or we don't
+ * know how to dismiss keyguard from the current state.
*/
- fun startDismissKeyguardTransition(reason: String = "") {
- if (SceneContainerFlag.isEnabled) return
+ fun startDismissKeyguardTransition(reason: String = ""): Boolean {
+ if (SceneContainerFlag.isEnabled) return false
Log.d(TAG, "#startDismissKeyguardTransition(reason=$reason)")
val startedState =
if (transitionRaceCondition()) {
@@ -65,13 +68,20 @@
AOD -> fromAodTransitionInteractor.dismissAod()
DOZING -> fromDozingTransitionInteractor.dismissFromDozing()
KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.dismissFromOccluded()
- KeyguardState.GONE ->
+ KeyguardState.GONE -> {
Log.i(
TAG,
"Already transitioning to GONE; ignoring startDismissKeyguardTransition.",
)
- else -> Log.e(TAG, "We don't know how to dismiss keyguard from state $startedState.")
+ return false
+ }
+ else -> {
+ Log.e(TAG, "We don't know how to dismiss keyguard from state $startedState.")
+ return false
+ }
}
+
+ return true
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
index 2497def..d981eeb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToOccludedTransitionViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.util.MathUtils
+import com.android.systemui.Flags.lightRevealMigration
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -32,9 +33,7 @@
@SysUISingleton
class AodToOccludedTransitionViewModel
@Inject
-constructor(
- animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow.setup(
duration = FromAodTransitionInteractor.TO_OCCLUDED_DURATION,
@@ -52,10 +51,20 @@
var currentAlpha = 0f
return transitionAnimation.sharedFlow(
duration = 250.milliseconds,
- startTime = 100.milliseconds, // Wait for the light reveal to "hit" the LS elements.
- onStart = { currentAlpha = viewState.alpha() },
+ startTime =
+ if (lightRevealMigration()) {
+ 100.milliseconds // Wait for the light reveal to "hit" the LS elements.
+ } else {
+ 0.milliseconds
+ },
+ onStart = {
+ if (lightRevealMigration()) {
+ currentAlpha = viewState.alpha()
+ } else {
+ currentAlpha = 0f
+ }
+ },
onStep = { MathUtils.lerp(currentAlpha, 0f, it) },
- onCancel = { 0f },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index af37eea..1a2238c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -234,8 +234,6 @@
(ViewGroup.MarginLayoutParams) mItemLayout.getLayoutParams();
params.rightMargin = showEndTouchArea ? mController.getItemMarginEndSelectable()
: mController.getItemMarginEndDefault();
- mTitleIcon.setBackgroundTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
}
void setTwoLineLayout(MediaDevice device, boolean bFocused, boolean showSeekBar,
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 1dbb317..15afd22 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -37,9 +37,6 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.AudioManager;
@@ -535,17 +532,9 @@
// Use default Bluetooth device icon to handle getIcon() is null case.
drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
}
- if (!(drawable instanceof BitmapDrawable)) {
- setColorFilter(drawable, isActiveItem(device));
- }
return BluetoothUtils.createIconWithDrawable(drawable);
}
- void setColorFilter(Drawable drawable, boolean isActive) {
- drawable.setColorFilter(new PorterDuffColorFilter(mColorItemContent,
- PorterDuff.Mode.SRC_IN));
- }
-
boolean isActiveItem(MediaDevice device) {
boolean isConnected = mLocalMediaManager.getCurrentConnectedDevice().getId().equals(
device.getId());
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index e9b7534..3f14b55e 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -44,6 +44,7 @@
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.InputMethodService.BackDispositionMode;
import android.inputmethodservice.InputMethodService.ImeWindowVisibility;
+import android.os.Handler;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Log;
@@ -61,6 +62,7 @@
import com.android.internal.view.AppearanceRegion;
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
@@ -182,15 +184,18 @@
private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private final StatusBarStateController mStatusBarStateController;
private DisplayTracker mDisplayTracker;
+ private final Handler mBgHandler;
@Inject
public TaskbarDelegate(Context context,
LightBarTransitionsController.Factory lightBarTransitionsControllerFactory,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
- StatusBarStateController statusBarStateController) {
+ StatusBarStateController statusBarStateController,
+ @Background Handler bgHandler) {
mLightBarTransitionsControllerFactory = lightBarTransitionsControllerFactory;
mContext = context;
+ mBgHandler = bgHandler;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mPipListener = (bounds) -> {
mEdgeBackGestureHandler.setPipStashExclusionBounds(bounds);
@@ -245,7 +250,9 @@
new LightBarTransitionsController.DarkIntensityApplier() {
@Override
public void applyDarkIntensity(float darkIntensity) {
- mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
+ mBgHandler.post(() -> {
+ mOverviewProxyService.onNavButtonsDarkIntensityChanged(darkIntensity);
+ });
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index d401b6e..a98b8e5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -784,7 +784,11 @@
private fun updateLongPressEffect(handlesLongClick: Boolean) {
// The long press effect in the tile can't be updated if it is still running
- if (longPressEffect?.state != QSLongPressEffect.State.IDLE) return
+ if (
+ longPressEffect?.state != QSLongPressEffect.State.IDLE &&
+ longPressEffect?.state != QSLongPressEffect.State.CLICKED
+ )
+ return
longPressEffect.qsTile?.state?.handlesLongClick = handlesLongClick
if (handlesLongClick && longPressEffect.initializeEffect(longPressEffectDuration)) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
index dc0c4dc..2bcd1d1 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlag.kt
@@ -24,7 +24,6 @@
import com.android.systemui.flags.RefactorFlagUtils
import com.android.systemui.keyguard.KeyguardWmStateRefactor
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
-import com.android.systemui.statusbar.phone.PredictiveBackSysUiFlag
/** Helper for reading or using the scene container flag state. */
object SceneContainerFlag {
@@ -36,8 +35,7 @@
get() =
sceneContainer() && // mainAconfigFlag
KeyguardWmStateRefactor.isEnabled &&
- NotificationThrottleHun.isEnabled &&
- PredictiveBackSysUiFlag.isEnabled
+ NotificationThrottleHun.isEnabled
// NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer
@@ -49,7 +47,6 @@
sequenceOf(
KeyguardWmStateRefactor.token,
NotificationThrottleHun.token,
- PredictiveBackSysUiFlag.token,
// NOTE: Changes should also be made in isEnabled and @EnableSceneContainer
)
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
index e5ff252..f295c0c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -19,6 +19,7 @@
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.os.UserHandle
+import android.view.View
import androidx.annotation.StyleRes
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
@@ -119,6 +120,12 @@
super<BaseMediaProjectionPermissionDialogDelegate>.onCreate(dialog, savedInstanceState)
setDialogTitle(R.string.screenrecord_permission_dialog_title)
dialog.setTitle(R.string.screenrecord_title)
+ setStartButtonOnClickListener { v: View? ->
+ val screenRecordViewBinder: ScreenRecordPermissionViewBinder? =
+ viewBinder as ScreenRecordPermissionViewBinder?
+ screenRecordViewBinder?.startButtonOnClicked()
+ dialog.dismiss()
+ }
setCancelButtonOnClickListener { dialog.dismiss() }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
index 9f7e1ad..691bdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionViewBinder.kt
@@ -79,37 +79,38 @@
override fun bind() {
super.bind()
initRecordOptionsView()
- setStartButtonOnClickListener { _: View? ->
- onStartRecordingClicked?.run()
- if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
- requestScreenCapture(
- captureTarget = null,
- displayId = selectedScreenShareOption.displayId,
- )
- }
- if (selectedScreenShareOption.mode == SINGLE_APP) {
- val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ setStartButtonOnClickListener { startButtonOnClicked() }
+ }
- // We can't start activity for result here so we use result receiver to get
- // the selected target to capture
- intent.putExtra(
- MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
- CaptureTargetResultReceiver(),
- )
+ fun startButtonOnClicked() {
+ onStartRecordingClicked?.run()
+ if (selectedScreenShareOption.mode == ENTIRE_SCREEN) {
+ requestScreenCapture(
+ captureTarget = null,
+ displayId = selectedScreenShareOption.displayId,
+ )
+ }
+ if (selectedScreenShareOption.mode == SINGLE_APP) {
+ val intent = Intent(dialog.context, MediaProjectionAppSelectorActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- intent.putExtra(
- MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
- hostUserHandle,
- )
- intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
- intent.putExtra(
- MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
- MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
- )
- activityStarter.startActivity(intent, /* dismissShade= */ true)
- }
- dialog.dismiss()
+ // We can't start activity for result here so we use result receiver to get
+ // the selected target to capture
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
+ CaptureTargetResultReceiver(),
+ )
+
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
+ hostUserHandle,
+ )
+ intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_UID, hostUid)
+ intent.putExtra(
+ MediaProjectionAppSelectorActivity.EXTRA_SCREEN_SHARE_TYPE,
+ MediaProjectionAppSelectorActivity.ScreenShareType.ScreenRecord.name,
+ )
+ activityStarter.startActivity(intent, /* dismissShade= */ true)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
index e1631cc..bbb13d5 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt
@@ -61,9 +61,18 @@
/** Callback for notifying of changes. */
@WeaklyReferencedCallback
interface Callback {
- /** Notifies that the current user will be changed. */
+ /**
+ * Same as {@link onBeforeUserSwitching(Int, Runnable)} but the callback will be called
+ * automatically after the completion of this method.
+ */
fun onBeforeUserSwitching(newUser: Int) {}
+ /** Notifies that the current user will be changed. */
+ fun onBeforeUserSwitching(newUser: Int, resultCallback: Runnable) {
+ onBeforeUserSwitching(newUser)
+ resultCallback.run()
+ }
+
/**
* Same as {@link onUserChanging(Int, Context, Runnable)} but the callback will be called
* automatically after the completion of this method.
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index b7a3aed..42d8363 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -196,8 +196,9 @@
private fun registerUserSwitchObserver() {
iActivityManager.registerUserSwitchObserver(
object : UserSwitchObserver() {
- override fun onBeforeUserSwitching(newUserId: Int) {
+ override fun onBeforeUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
handleBeforeUserSwitching(newUserId)
+ reply?.sendResult(null)
}
override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) {
@@ -236,8 +237,7 @@
setUserIdInternal(newUserId)
notifySubscribers { callback, resultCallback ->
- callback.onBeforeUserSwitching(newUserId)
- resultCallback.run()
+ callback.onBeforeUserSwitching(newUserId, resultCallback)
}
.await()
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index ef62d2d..a2edd3ab 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -15,11 +15,18 @@
*/
package com.android.systemui.shade.data.repository
+import android.annotation.SuppressLint
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
/** Data for the shade, mostly related to expansion of the shade and quick settings. */
interface ShadeRepository {
@@ -36,7 +43,7 @@
* Information about the currently running fling animation, or null if no fling animation is
* running.
*/
- val currentFling: StateFlow<FlingInfo?>
+ val currentFling: SharedFlow<FlingInfo?>
/**
* The amount the lockscreen shade has dragged down by the user, [0-1]. 0 means fully collapsed,
@@ -180,7 +187,8 @@
/** Business logic for shade interactions */
@SysUISingleton
-class ShadeRepositoryImpl @Inject constructor() : ShadeRepository {
+class ShadeRepositoryImpl @Inject constructor(@Background val backgroundScope: CoroutineScope) :
+ ShadeRepository {
private val _qsExpansion = MutableStateFlow(0f)
@Deprecated("Use ShadeInteractor.qsExpansion instead")
override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow()
@@ -193,8 +201,13 @@
override val udfpsTransitionToFullShadeProgress: StateFlow<Float> =
_udfpsTransitionToFullShadeProgress.asStateFlow()
- private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
- override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+ /**
+ * Must be a SharedFlow, since the fling is by definition an event and dropping it has extreme
+ * consequences in some cases (for example, keyguard uses this to decide when to unlock).
+ */
+ @SuppressLint("SharedFlowCreation")
+ override val currentFling: MutableSharedFlow<FlingInfo?> =
+ MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val _legacyShadeExpansion = MutableStateFlow(0f)
@Deprecated("Use ShadeInteractor.shadeExpansion instead")
@@ -294,7 +307,7 @@
}
override fun setCurrentFling(info: FlingInfo?) {
- _currentFling.value = info
+ backgroundScope.launch { currentFling.emit(info) }
}
@Deprecated("Should only be called by NPVC and tests")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 239257d..2bcd3fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -61,11 +61,13 @@
import com.android.systemui.Flags;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlagsClassic;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.recents.OverviewProxyService;
@@ -78,6 +80,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ListenerSet;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
import com.android.systemui.util.settings.SecureSettings;
import dagger.Lazy;
@@ -88,9 +91,13 @@
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Handles keeping track of the current user, profiles, and various things related to hiding
* contents, redacting notifications, and the lockscreen.
@@ -111,6 +118,9 @@
private static final Uri SHOW_PRIVATE_LOCKSCREEN =
Settings.Secure.getUriFor(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+ private static final long LOCK_TIME_FOR_SENSITIVE_REDACTION_MS =
+ TimeUnit.MINUTES.toMillis(10);
+
private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy;
private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy;
private final DevicePolicyManager mDevicePolicyManager;
@@ -284,7 +294,12 @@
protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>();
+ // The last lock time. Uses currentTimeMillis
+ @VisibleForTesting
+ protected final AtomicLong mLastLockTime = new AtomicLong(-1);
+
protected int mCurrentUserId = 0;
+
protected NotificationPresenter mPresenter;
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
@@ -311,7 +326,10 @@
DumpManager dumpManager,
LockPatternUtils lockPatternUtils,
FeatureFlagsClassic featureFlags,
- Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) {
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
+ Lazy<KeyguardInteractor> keyguardInteractor,
+ @Application CoroutineScope coroutineScope
+ ) {
mContext = context;
mMainExecutor = mainExecutor;
mBackgroundExecutor = backgroundExecutor;
@@ -341,6 +359,18 @@
if (keyguardPrivateNotifications()) {
init();
}
+
+ // To avoid dependency injection cycle, finish constructing this object before using the
+ // KeyguardInteractor. The CoroutineScope will only be null in tests.
+ if (LockscreenOtpRedaction.isEnabled() && coroutineScope != null) {
+ mMainExecutor.execute(() -> JavaAdapterKt.collectFlow(coroutineScope,
+ keyguardInteractor.get().isKeyguardDismissible(),
+ unlocked -> {
+ if (!unlocked) {
+ mLastLockTime.set(System.currentTimeMillis());
+ }
+ }));
+ }
}
public void setUpWithPresenter(NotificationPresenter presenter) {
@@ -443,7 +473,7 @@
mCurrentUserId = mUserTracker.getUserId(); // in case we reg'd receiver too late
updateCurrentProfilesCache();
- // Set up
+ // Set up
mBackgroundExecutor.execute(() -> {
@SuppressLint("MissingPermission") List<UserInfo> users = mUserManager.getUsers();
for (int i = users.size() - 1; i >= 0; i--) {
@@ -667,8 +697,6 @@
!userAllowsPrivateNotificationsInPublic(mCurrentUserId);
boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId);
boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId);
- boolean isNotifSensitive = LockscreenOtpRedaction.isEnabled()
- && ent.getRanking() != null && ent.getRanking().hasSensitiveContent();
// redact notifications if the current user is redacting notifications or the notification
// contains sensitive content. However if the notification is associated with a managed
@@ -689,12 +717,45 @@
if (keyguardPrivateNotifications() && !mKeyguardAllowingNotifications) {
return REDACTION_TYPE_PUBLIC;
}
- if (isNotifSensitive) {
+
+ if (shouldShowSensitiveContentRedactedView(ent)) {
return REDACTION_TYPE_SENSITIVE_CONTENT;
}
return REDACTION_TYPE_NONE;
}
+ /*
+ * We show the sensitive content redaction view if
+ * 1. The feature is enabled
+ * 2. The device is locked
+ * 3. The notification has the `hasSensitiveContent` ranking variable set to true
+ * 4. The device has been locked for at least LOCK_TIME_FOR_SENSITIVE_REDACTION_MS
+ * 5. The notification arrived since the last lock time
+ */
+ private boolean shouldShowSensitiveContentRedactedView(NotificationEntry ent) {
+ if (!LockscreenOtpRedaction.isEnabled()) {
+ return false;
+ }
+
+ if (!mKeyguardManager.isDeviceLocked()) {
+ return false;
+ }
+
+ if (ent.getRanking() == null || !ent.getRanking().hasSensitiveContent()) {
+ return false;
+ }
+
+ long lastLockedTime = mLastLockTime.get();
+ if (ent.getSbn().getPostTime() < lastLockedTime) {
+ return false;
+ }
+
+ if ((System.currentTimeMillis() - lastLockedTime) < LOCK_TIME_FOR_SENSITIVE_REDACTION_MS) {
+ return false;
+ }
+ return true;
+ }
+
private boolean packageHasVisibilityOverride(String key) {
if (mCommonNotifCollectionLazy.get() == null) {
Log.wtf(TAG, "mEntryManager was null!", new Throwable());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
index 059e69a..69ef09d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt
@@ -242,8 +242,9 @@
chipTimeView: ChipChronometer,
chipShortTimeDeltaView: DateTimeView,
) {
- if (chipModel.icon != null) {
- if (chipModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView) {
+ val icon = chipModel.icon
+ if (icon != null) {
+ if (iconRequiresEmbeddedPadding(icon)) {
// If the icon is a custom [StatusBarIconView], then it should've come from
// `Notification.smallIcon`, which is required to embed its own paddings. We need to
// adjust the other paddings to make everything look good :)
@@ -265,6 +266,10 @@
}
}
+ private fun iconRequiresEmbeddedPadding(icon: OngoingActivityChipModel.ChipIcon) =
+ icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
+ icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+
private fun View.setTextPaddingForEmbeddedPaddingIcon() {
val newPaddingEnd =
context.resources.getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
new file mode 100644
index 0000000..3d768d2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.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.statusbar.chips.ui.compose
+
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.rememberTextMeasurer
+import com.android.systemui.res.R
+
+/**
+ * Renders text within a status bar chip. The text is only displayed if more than 50% of its width
+ * can fit inside the bounds of the chip. If there is any overflow,
+ * [R.dimen.ongoing_activity_chip_text_fading_edge_length] is used to fade out the edge of the text.
+ */
+@Composable
+fun ChipText(
+ text: String,
+ backgroundColor: Color,
+ modifier: Modifier = Modifier,
+ color: Color = Color.Unspecified,
+ style: TextStyle = LocalTextStyle.current,
+ minimumVisibleRatio: Float = 0.5f,
+) {
+ val density = LocalDensity.current
+ val textMeasurer = rememberTextMeasurer()
+
+ val textFadeLength =
+ dimensionResource(id = R.dimen.ongoing_activity_chip_text_fading_edge_length)
+ val maxTextWidthDp = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
+ val maxTextWidthPx = with(density) { maxTextWidthDp.toPx() }
+
+ val textLayoutResult = remember(text, style) { textMeasurer.measure(text, style) }
+ val willOverflowWidth = textLayoutResult.size.width > maxTextWidthPx
+
+ if (isSufficientlyVisible(maxTextWidthPx, minimumVisibleRatio, textLayoutResult)) {
+ Text(
+ text = text,
+ style = style,
+ softWrap = false,
+ color = color,
+ modifier =
+ modifier
+ .sizeIn(maxWidth = maxTextWidthDp)
+ .then(
+ if (willOverflowWidth) {
+ Modifier.overflowFadeOut(
+ with(density) { textFadeLength.roundToPx() },
+ backgroundColor,
+ )
+ } else {
+ Modifier
+ }
+ ),
+ )
+ }
+}
+
+private fun Modifier.overflowFadeOut(fadeLength: Int, color: Color): Modifier = drawWithContent {
+ drawContent()
+
+ val brush =
+ Brush.horizontalGradient(
+ colors = listOf(Color.Transparent, color),
+ startX = size.width - fadeLength,
+ endX = size.width,
+ )
+ drawRect(
+ brush = brush,
+ topLeft = Offset(size.width - fadeLength, 0f),
+ size = Size(fadeLength.toFloat(), size.height),
+ )
+}
+
+/**
+ * Returns `true` if at least [minimumVisibleRatio] of the text width fits within the given
+ * [maxAvailableWidthPx].
+ */
+@Composable
+private fun isSufficientlyVisible(
+ maxAvailableWidthPx: Float,
+ minimumVisibleRatio: Float,
+ textLayoutResult: TextLayoutResult,
+): Boolean {
+ val widthPx = textLayoutResult.size.width
+
+ return (maxAvailableWidthPx / widthPx) > minimumVisibleRatio
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 254b792..d327fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.dagger;
-import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
-
import android.content.Context;
import android.os.Handler;
import android.os.RemoteException;
@@ -28,7 +26,6 @@
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.CoreStartable;
import com.android.systemui.animation.ActivityTransitionAnimator;
-import com.android.systemui.animation.AnimationFeatureFlags;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
@@ -226,8 +223,7 @@
IDreamManager dreamManager,
KeyguardStateController keyguardStateController,
Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
- InteractionJankMonitor interactionJankMonitor,
- AnimationFeatureFlags animationFeatureFlags) {
+ InteractionJankMonitor interactionJankMonitor) {
DialogTransitionAnimator.Callback callback = new DialogTransitionAnimator.Callback() {
@Override
public boolean isDreaming() {
@@ -249,19 +245,6 @@
return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint();
}
};
- return new DialogTransitionAnimator(
- mainExecutor, callback, interactionJankMonitor, animationFeatureFlags);
- }
-
- /** */
- @Provides
- @SysUISingleton
- static AnimationFeatureFlags provideAnimationFeatureFlags() {
- return new AnimationFeatureFlags() {
- @Override
- public boolean isPredictiveBackQsDialogAnim() {
- return predictiveBackAnimateDialogs();
- }
- };
+ return new DialogTransitionAnimator(mainExecutor, callback, interactionJankMonitor);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index 46c84fbc..eff959d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -27,12 +27,12 @@
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.StatusBarDataLayerModule
import com.android.systemui.statusbar.data.repository.LightBarControllerStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
import com.android.systemui.statusbar.phone.AutoHideController
import com.android.systemui.statusbar.phone.AutoHideControllerImpl
import com.android.systemui.statusbar.phone.LightBarController
import com.android.systemui.statusbar.phone.LightBarControllerImpl
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
@@ -91,9 +91,7 @@
@SysUISingleton
@IntoMap
@ClassKey(OngoingCallController::class)
- fun ongoingCallController(
- controller: OngoingCallController
- ): CoreStartable =
+ fun ongoingCallController(controller: OngoingCallController): CoreStartable =
if (StatusBarChipsModernization.isEnabled) {
CoreStartable.NOP
} else {
@@ -104,9 +102,7 @@
@SysUISingleton
@IntoMap
@ClassKey(OngoingCallInteractor::class)
- fun ongoingCallInteractor(
- interactor: OngoingCallInteractor
- ): CoreStartable =
+ fun ongoingCallInteractor(interactor: OngoingCallInteractor): CoreStartable =
if (StatusBarChipsModernization.isEnabled) {
interactor
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
index 0cd31d0..b7b91fa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarAppearance.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.data.model
import com.android.internal.view.AppearanceRegion
-import com.android.systemui.statusbar.phone.BoundsPair
+import com.android.systemui.statusbar.layout.BoundsPair
/** Keeps track of various parameters coordinating the appearance of the status bar. */
data class StatusBarAppearance(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
index 554c46f..5ea1211 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStore.kt
@@ -28,8 +28,8 @@
import com.android.systemui.display.data.repository.PerDisplayStoreImpl
import com.android.systemui.display.data.repository.SingleDisplayStore
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl
import dagger.Lazy
import dagger.Module
import dagger.Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
index 22c37df..7fa9f0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
@@ -33,9 +33,9 @@
import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
import com.android.systemui.statusbar.data.model.StatusBarAppearance
import com.android.systemui.statusbar.data.model.StatusBarMode
-import com.android.systemui.statusbar.phone.BoundsPair
-import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.BoundsPair
+import com.android.systemui.statusbar.layout.LetterboxAppearanceCalculator
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
import com.android.systemui.statusbar.phone.fragment.dagger.HomeStatusBarComponent
import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
@@ -209,9 +209,7 @@
override val ongoingProcessRequiresStatusBarVisible =
_ongoingProcessRequiresStatusBarVisible.asStateFlow()
- override fun setOngoingProcessRequiresStatusBarVisible(
- requiredVisible: Boolean
- ) {
+ override fun setOngoingProcessRequiresStatusBarVisible(requiredVisible: Boolean) {
_ongoingProcessRequiresStatusBarVisible.value = requiredVisible
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index f7bc23c..63410d746 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -41,8 +41,8 @@
import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft
import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.leak.RotationUtils
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 1038ad4..70632b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -36,8 +36,8 @@
import com.android.systemui.res.R
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
index 231a8c6..1469fe7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxAppearanceCalculator.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.annotation.ColorInt
import android.content.Context
@@ -39,7 +39,7 @@
) {
override fun toString(): String {
val appearanceString =
- ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
+ ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
return "LetterboxAppearance{$appearanceString, $appearanceRegions}"
}
}
@@ -57,14 +57,16 @@
private val letterboxBackgroundProvider: LetterboxBackgroundProvider,
) : Dumpable {
- private val darkAppearanceIconColor = context.getColor(
- // For a dark background status bar, use a *light* icon color.
- com.android.settingslib.R.color.light_mode_icon_color_single_tone
- )
- private val lightAppearanceIconColor = context.getColor(
- // For a light background status bar, use a *dark* icon color.
- com.android.settingslib.R.color.dark_mode_icon_color_single_tone
- )
+ private val darkAppearanceIconColor =
+ context.getColor(
+ // For a dark background status bar, use a *light* icon color.
+ com.android.settingslib.R.color.light_mode_icon_color_single_tone
+ )
+ private val lightAppearanceIconColor =
+ context.getColor(
+ // For a light background status bar, use a *dark* icon color.
+ com.android.settingslib.R.color.dark_mode_icon_color_single_tone
+ )
init {
dumpManager.registerCriticalDumpable(this)
@@ -85,7 +87,11 @@
lastAppearanceRegions = originalAppearanceRegions
lastLetterboxes = letterboxes
return getLetterboxAppearanceInternal(
- letterboxes, originalAppearance, originalAppearanceRegions, statusBarBounds)
+ letterboxes,
+ originalAppearance,
+ originalAppearanceRegions,
+ statusBarBounds,
+ )
.also { lastLetterboxAppearance = it }
}
@@ -118,7 +124,7 @@
private fun getAppearanceRegions(
originalAppearanceRegions: List<AppearanceRegion>,
- letterboxes: List<LetterboxDetails>
+ letterboxes: List<LetterboxDetails>,
): List<AppearanceRegion> {
return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
getAllOuterAppearanceRegions(letterboxes)
@@ -126,7 +132,7 @@
private fun sanitizeAppearanceRegions(
originalAppearanceRegions: List<AppearanceRegion>,
- letterboxes: List<LetterboxDetails>
+ letterboxes: List<LetterboxDetails>,
): List<AppearanceRegion> =
originalAppearanceRegions.map { appearanceRegion ->
val matchingLetterbox =
@@ -138,17 +144,20 @@
// full bounds of its window.
// Here we want the bounds to be only for the inner bounds of the letterboxed app.
AppearanceRegion(
- appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
+ appearanceRegion.appearance,
+ matchingLetterbox.letterboxInnerBounds,
+ )
}
}
private fun originalAppearanceWithScrim(
@Appearance originalAppearance: Int,
- originalAppearanceRegions: List<AppearanceRegion>
+ originalAppearanceRegions: List<AppearanceRegion>,
): LetterboxAppearance {
return LetterboxAppearance(
originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
- originalAppearanceRegions)
+ originalAppearanceRegions,
+ )
}
@Appearance
@@ -215,7 +224,9 @@
lastAppearanceRegion: $lastAppearanceRegions,
lastLetterboxes: $lastLetterboxes,
lastLetterboxAppearance: $lastLetterboxAppearance
- """.trimIndent())
+ """
+ .trimIndent()
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
similarity index 93%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
index 34c7059e..3d8ced1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/LetterboxBackgroundProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.annotation.ColorInt
import android.app.WallpaperManager
@@ -49,9 +49,7 @@
private set
private val wallpaperColorsListener =
- WallpaperManager.OnColorsChangedListener { _, _ ->
- fetchBackgroundColorInfo()
- }
+ WallpaperManager.OnColorsChangedListener { _, _ -> fetchBackgroundColorInfo() }
override fun start() {
fetchBackgroundColorInfo()
@@ -75,6 +73,8 @@
"""
letterboxBackgroundColor: ${Color.valueOf(letterboxBackgroundColor)}
isLetterboxBackgroundMultiColored: $isLetterboxBackgroundMultiColored
- """.trimIndent())
+ """
+ .trimIndent()
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
similarity index 98%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
index 3ac0bac..ac5b037 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarBoundsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarBoundsProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.graphics.Rect
import android.view.View
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
similarity index 99%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
index 41db5f4..f7a9094 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.annotation.Px
import android.content.Context
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 9766f9e..071d232 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -2663,12 +2663,6 @@
}
@Override
- public int getHeadsUpInset() {
- if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0;
- return mHeadsUpInset;
- }
-
- @Override
public int getStackBottomInset() {
return mPaddingBetweenElements + mShelf.getIntrinsicHeight();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 5249a6d..d302fb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -106,9 +106,6 @@
/** Sets whether the view is displayed in pulsing mode. */
fun setPulsing(pulsing: Boolean, animated: Boolean)
- /** Gets the inset for HUNs when they are not visible */
- fun getHeadsUpInset(): Int
-
/**
* Signals that any open Notification guts should be closed, as scene container is handling
* touch events.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index aba30d2..1474789 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -851,10 +851,6 @@
mLightRevealScrim = lightRevealScrim;
mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
-
- if (PredictiveBackSysUiFlag.isEnabled()) {
- mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
- }
}
private void initBubbles(Bubbles bubbles) {
@@ -3031,9 +3027,6 @@
public void onConfigChanged(Configuration newConfig) {
updateResources();
updateDisplaySize(); // populates mDisplayMetrics
- if (PredictiveBackSysUiFlag.isEnabled()) {
- mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
- }
if (DEBUG) {
Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index 6a77988..a339bc9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -47,6 +47,7 @@
import com.android.systemui.battery.BatteryMeterView;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange;
import com.android.systemui.statusbar.phone.ui.TintedIconManager;
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 6028f17..4c2bfe5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -64,6 +64,7 @@
import com.android.systemui.statusbar.disableflags.DisableStateTracker;
import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.stack.AnimationProperties;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
index 2e3f0d0..1e6a0f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxModule.kt
@@ -18,6 +18,7 @@
package com.android.systemui.statusbar.phone
import com.android.systemui.CoreStartable
+import com.android.systemui.statusbar.layout.LetterboxBackgroundProvider
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
index ca0c1ac9..0a28551 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarControllerImpl.java
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore;
import com.android.systemui.statusbar.data.repository.StatusBarModePerDisplayRepository;
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
+import com.android.systemui.statusbar.layout.BoundsPair;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.util.Compile;
import com.android.systemui.util.kotlin.JavaAdapterKt;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index ccd1b6c..aa13089 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -46,6 +46,7 @@
import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.Clock
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.window.StatusBarWindowStateController
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
deleted file mode 100644
index 74d6ba5..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PredictiveBackSysUiFlag.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.phone
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the predictive back flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object PredictiveBackSysUiFlag {
- /** The aconfig flag name */
- const val FLAG_NAME = Flags.FLAG_PREDICTIVE_BACK_SYSUI
-
- /** A token used for dependency declaration */
- val token: FlagToken
- get() = FlagToken(FLAG_NAME, isEnabled)
-
- /** Is the refactor enabled */
- @JvmStatic
- inline val isEnabled
- get() = Flags.predictiveBackSysui()
-
- /**
- * 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/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 3749b96..8443edd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -18,7 +18,6 @@
import static android.view.WindowInsets.Type.navigationBars;
-import static com.android.systemui.Flags.predictiveBackAnimateBouncer;
import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
import static com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
@@ -328,7 +327,6 @@
private float mQsExpansion;
final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
- private boolean mIsBackAnimationEnabled;
private final UdfpsOverlayInteractor mUdfpsOverlayInteractor;
private final ActivityStarter mActivityStarter;
@@ -434,7 +432,6 @@
.map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
mAlternateBouncerInteractor = alternateBouncerInteractor;
mBouncerInteractor = bouncerInteractor;
- mIsBackAnimationEnabled = predictiveBackAnimateBouncer();
mUdfpsOverlayInteractor = udfpsOverlayInteractor;
mActivityStarter = activityStarter;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
@@ -630,7 +627,7 @@
private boolean shouldPlayBackAnimation() {
// Suppress back animation when bouncer shouldn't be dismissed on back invocation.
- return !needsFullscreenBouncer() && mIsBackAnimationEnabled;
+ return !needsFullscreenBouncer();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 03324d2..c47ed17 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.phone;
-import static com.android.systemui.Flags.predictiveBackAnimateDialogs;
-
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
@@ -285,15 +283,13 @@
for (int i = 0; i < mOnCreateRunnables.size(); i++) {
mOnCreateRunnables.get(i).run();
}
- if (predictiveBackAnimateDialogs()) {
- View targetView = getWindow().getDecorView();
- DialogKt.registerAnimationOnBackInvoked(
- /* dialog = */ this,
- /* targetView = */ targetView,
- /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
- () -> targetView.getResources().getDisplayMetrics())
- );
- }
+ View targetView = getWindow().getDecorView();
+ DialogKt.registerAnimationOnBackInvoked(
+ /* dialog = */ this,
+ /* targetView = */ targetView,
+ /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
+ () -> targetView.getResources().getDisplayMetrics())
+ );
}
private void updateWindowSize() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
index 5837752..7207d0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/HomeStatusBarComponent.java
@@ -21,13 +21,13 @@
import com.android.systemui.dagger.qualifiers.RootView;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider;
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
import com.android.systemui.statusbar.phone.LegacyLightsOutNotifController;
import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
import com.android.systemui.statusbar.phone.StatusBarDemoMode;
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
import com.android.systemui.statusbar.window.StatusBarWindowController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
index ba91814..b56a9a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarStartablesModule.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.phone.fragment.dagger
-import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
+import com.android.systemui.statusbar.layout.StatusBarBoundsProvider
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index 12ed647..fdc2d8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -16,8 +16,8 @@
package com.android.systemui.statusbar.policy.domain.interactor
-import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
import android.content.Context
+import android.media.AudioManager
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
@@ -29,6 +29,7 @@
import com.android.settingslib.notification.modes.ZenIcon
import com.android.settingslib.notification.modes.ZenIconLoader
import com.android.settingslib.notification.modes.ZenMode
+import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
@@ -67,6 +68,17 @@
deviceProvisioningRepository: DeviceProvisioningRepository,
userSetupRepository: UserSetupRepository,
) {
+ /**
+ * List of predicates to determine if the [ZenMode] blocks an audio stream. Typical use case
+ * would be: `zenModeByStreamPredicates[stream](zenMode)`
+ */
+ private val zenModeByStreamPredicates =
+ mapOf<Int, (ZenMode) -> Boolean>(
+ AudioManager.STREAM_MUSIC to { it.policy.priorityCategoryMedia == STATE_DISALLOW },
+ AudioManager.STREAM_ALARM to { it.policy.priorityCategoryAlarms == STATE_DISALLOW },
+ AudioManager.STREAM_SYSTEM to { it.policy.priorityCategorySystem == STATE_DISALLOW },
+ )
+
val isZenAvailable: Flow<Boolean> =
combine(
deviceProvisioningRepository.isDeviceProvisioned,
@@ -125,21 +137,16 @@
.flowOn(bgDispatcher)
.distinctUntilChanged()
- val activeModesBlockingEverything: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
- mode.interruptionFilter == INTERRUPTION_FILTER_NONE
- }
+ fun canBeBlockedByZenMode(stream: AudioStream): Boolean =
+ zenModeByStreamPredicates.containsKey(stream.value)
- val activeModesBlockingMedia: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
- mode.policy.priorityCategoryMedia == STATE_DISALLOW
- }
-
- val activeModesBlockingAlarms: Flow<ActiveZenModes> = getFilteredActiveModesFlow { mode ->
- mode.policy.priorityCategoryAlarms == STATE_DISALLOW
- }
-
- private fun getFilteredActiveModesFlow(predicate: (ZenMode) -> Boolean): Flow<ActiveZenModes> {
+ fun activeModesBlockingStream(stream: AudioStream): Flow<ActiveZenModes> {
+ val isBlockingStream = zenModeByStreamPredicates[stream.value]
+ require(isBlockingStream != null) {
+ "$stream is unsupported. Use canBeBlockedByZenMode to check if the stream can be affected by the Zen Mode."
+ }
return modes
- .map { modes -> modes.filter { mode -> predicate(mode) } }
+ .map { modes -> modes.filter { isBlockingStream(it) } }
.map { modes -> buildActiveZenModes(modes) }
.flowOn(bgDispatcher)
.distinctUntilChanged()
@@ -194,7 +201,6 @@
)
null
}
-
ZEN_DURATION_FOREVER -> null
else -> Duration.ofMinutes(zenDuration.toLong())
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
index 1e043ec..ecfcb29 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -23,7 +23,7 @@
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.fragments.FragmentHostManager
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import java.util.Optional
/** Encapsulates all logic for the status bar window state management. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 848e91d..8518acb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -58,7 +58,7 @@
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays;
import com.android.systemui.statusbar.core.StatusBarRootModernization;
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController;
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.window.StatusBarWindowModule.InternalWindowViewInflater;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.unfold.util.JankMonitorTransitionProgressListener;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index 67ffb06..65a683c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -82,7 +82,7 @@
// coerce the current value to the new value range before animating it. This prevents
// animating from the value that is outside of current [valueFrom, valueTo].
value = value.coerceIn(valueFrom, valueTo)
- setTrackIconActiveStart(model.iconRes)
+ trackIconActiveStart = model.icon
if (isInitialUpdate) {
value = model.value
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
index 5c39b6f..daf4c82 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt
@@ -16,13 +16,16 @@
package com.android.systemui.volume.dialog.sliders.ui.viewmodel
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
import android.media.AudioManager
import androidx.annotation.DrawableRes
-import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -31,11 +34,12 @@
class VolumeDialogSliderIconProvider
@Inject
constructor(
- private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
+ private val context: Context,
+ private val zenModeInteractor: ZenModeInteractor,
private val audioVolumeInteractor: AudioVolumeInteractor,
) {
- @DrawableRes
+ @SuppressLint("UseCompatLoadingForDrawables")
fun getStreamIcon(
stream: Int,
level: Int,
@@ -43,54 +47,71 @@
levelMax: Int,
isMuted: Boolean,
isRoutedToBluetooth: Boolean,
- ): Flow<Int> {
+ ): Flow<Drawable> {
return combine(
- notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)),
+ zenModeInteractor.activeModesBlockingStream(AudioStream(stream)),
ringerModeForStream(stream),
- ) { isZenMuted, ringerMode ->
- val isStreamOffline = level == 0 || isMuted
- if (isZenMuted) {
- // TODO(b/372466264) use icon for the corresponding zenmode
- return@combine com.android.internal.R.drawable.ic_qs_dnd
- }
- when (ringerMode?.value) {
- AudioManager.RINGER_MODE_VIBRATE ->
- return@combine R.drawable.ic_volume_ringer_vibrate
- AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off
- }
- if (isRoutedToBluetooth) {
- return@combine if (stream == AudioManager.STREAM_VOICE_CALL) {
- R.drawable.ic_volume_bt_sco
- } else {
- if (isStreamOffline) {
- R.drawable.ic_volume_media_bt_mute
- } else {
- R.drawable.ic_volume_media_bt
- }
- }
- }
-
- return@combine if (isStreamOffline) {
- getMutedIconForStream(stream) ?: getIconForStream(stream)
+ ) { activeModesBlockingStream, ringerMode ->
+ if (activeModesBlockingStream.mainMode?.icon != null) {
+ return@combine activeModesBlockingStream.mainMode.icon.drawable
} else {
- if (level < (levelMax + levelMin) / 2) {
- // This icon is different on TV
- R.drawable.ic_volume_media_low
- } else {
- getIconForStream(stream)
- }
+ context.getDrawable(
+ getIconRes(
+ stream,
+ level,
+ levelMin,
+ levelMax,
+ isMuted,
+ isRoutedToBluetooth,
+ ringerMode,
+ )
+ )!!
}
}
}
@DrawableRes
- private fun getMutedIconForStream(stream: Int): Int? {
- return when (stream) {
- AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
- AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
- AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
- AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
- else -> null
+ private fun getIconRes(
+ stream: Int,
+ level: Int,
+ levelMin: Int,
+ levelMax: Int,
+ isMuted: Boolean,
+ isRoutedToBluetooth: Boolean,
+ ringerMode: RingerMode?,
+ ): Int {
+ val isStreamOffline = level == 0 || isMuted
+ when (ringerMode?.value) {
+ AudioManager.RINGER_MODE_VIBRATE -> return R.drawable.ic_volume_ringer_vibrate
+ AudioManager.RINGER_MODE_SILENT -> return R.drawable.ic_ring_volume_off
+ }
+ if (isRoutedToBluetooth) {
+ return if (stream == AudioManager.STREAM_VOICE_CALL) {
+ R.drawable.ic_volume_bt_sco
+ } else {
+ if (isStreamOffline) {
+ R.drawable.ic_volume_media_bt_mute
+ } else {
+ R.drawable.ic_volume_media_bt
+ }
+ }
+ }
+
+ return if (isStreamOffline) {
+ when (stream) {
+ AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute
+ AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute
+ AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute
+ AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute
+ else -> null
+ } ?: getIconForStream(stream)
+ } else {
+ if (level < (levelMax + levelMin) / 2) {
+ // This icon is different on TV
+ R.drawable.ic_volume_media_low
+ } else {
+ getIconForStream(stream)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
index 5750c04..8df9e78 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -16,21 +16,21 @@
package com.android.systemui.volume.dialog.sliders.ui.viewmodel
-import androidx.annotation.DrawableRes
+import android.graphics.drawable.Drawable
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
data class VolumeDialogSliderStateModel(
val minValue: Float,
val maxValue: Float,
val value: Float,
- @DrawableRes val iconRes: Int,
+ val icon: Drawable,
)
-fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel {
+fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel {
return VolumeDialogSliderStateModel(
minValue = levelMin.toFloat(),
value = level.toFloat(),
maxValue = levelMax.toFloat(),
- iconRes = iconRes,
+ icon = icon,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index cec3d1e..5b8d9b0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -18,9 +18,6 @@
import android.content.Context
import android.media.AudioManager
-import android.media.AudioManager.STREAM_ALARM
-import android.media.AudioManager.STREAM_MUSIC
-import android.media.AudioManager.STREAM_NOTIFICATION
import android.util.Log
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEventLogger
@@ -34,8 +31,6 @@
import com.android.systemui.modes.shared.ModesUiIcons
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
-import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
-import com.android.systemui.util.kotlin.combine
import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
import dagger.assisted.Assisted
@@ -43,12 +38,15 @@
import dagger.assisted.AssistedInject
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
@@ -101,48 +99,16 @@
)
override val slider: StateFlow<SliderState> =
- if (ModesUiIcons.isEnabled) {
- combine(
- audioVolumeInteractor.getAudioStream(audioStream),
- audioVolumeInteractor.canChangeVolume(audioStream),
- audioVolumeInteractor.ringerMode,
- zenModeInteractor.activeModesBlockingEverything,
- zenModeInteractor.activeModesBlockingAlarms,
- zenModeInteractor.activeModesBlockingMedia,
- ) {
- model,
- isEnabled,
- ringerMode,
- modesBlockingEverything,
- modesBlockingAlarms,
- modesBlockingMedia ->
- volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
- model.toState(
- isEnabled,
- ringerMode,
- getStreamDisabledMessage(
- modesBlockingEverything,
- modesBlockingAlarms,
- modesBlockingMedia,
- ),
- )
- }
- .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
- } else {
- combine(
- audioVolumeInteractor.getAudioStream(audioStream),
- audioVolumeInteractor.canChangeVolume(audioStream),
- audioVolumeInteractor.ringerMode,
- ) { model, isEnabled, ringerMode ->
- volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
- model.toState(
- isEnabled,
- ringerMode,
- getStreamDisabledMessageWithoutModes(audioStream),
- )
- }
- .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
- }
+ combine(
+ audioVolumeInteractor.getAudioStream(audioStream),
+ audioVolumeInteractor.canChangeVolume(audioStream),
+ audioVolumeInteractor.ringerMode,
+ streamDisabledMessage(),
+ ) { model, isEnabled, ringerMode, streamDisabledMessage ->
+ volumePanelLogger.onVolumeUpdateReceived(audioStream, model.volume)
+ model.toState(isEnabled, ringerMode, streamDisabledMessage)
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
init {
volumeChanges
@@ -229,40 +195,32 @@
)
}
- private fun getStreamDisabledMessage(
- blockingEverything: ActiveZenModes,
- blockingAlarms: ActiveZenModes,
- blockingMedia: ActiveZenModes,
- ): String {
- // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
- // In fact, VOICE_CALL should not be affected by interruption filtering at all.
- return if (audioStream.value == STREAM_NOTIFICATION) {
- context.getString(R.string.stream_notification_unavailable)
- } else {
- val blockingModeName =
- when {
- blockingEverything.mainMode != null -> blockingEverything.mainMode.name
- audioStream.value == STREAM_ALARM -> blockingAlarms.mainMode?.name
- audioStream.value == STREAM_MUSIC -> blockingMedia.mainMode?.name
- else -> null
- }
-
- if (blockingModeName != null) {
- context.getString(R.string.stream_unavailable_by_modes, blockingModeName)
+ // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
+ // In fact, VOICE_CALL should not be affected by interruption filtering at all.
+ private fun streamDisabledMessage(): Flow<String> {
+ return if (ModesUiIcons.isEnabled) {
+ if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
+ flowOf(context.getString(R.string.stream_notification_unavailable))
} else {
- // Should not actually be visible, but as a catch-all.
- context.getString(R.string.stream_unavailable_by_unknown)
+ if (zenModeInteractor.canBeBlockedByZenMode(audioStream)) {
+ zenModeInteractor.activeModesBlockingStream(audioStream).map { blockingZenModes
+ ->
+ blockingZenModes.mainMode?.name?.let {
+ context.getString(R.string.stream_unavailable_by_modes, it)
+ } ?: context.getString(R.string.stream_unavailable_by_unknown)
+ }
+ } else {
+ flowOf(context.getString(R.string.stream_unavailable_by_unknown))
+ }
}
- }
- }
-
- private fun getStreamDisabledMessageWithoutModes(audioStream: AudioStream): String {
- // TODO: b/372213356 - Figure out the correct messages for VOICE_CALL and RING.
- // In fact, VOICE_CALL should not be affected by interruption filtering at all.
- return if (audioStream.value == STREAM_NOTIFICATION) {
- context.getString(R.string.stream_notification_unavailable)
} else {
- context.getString(R.string.stream_alarm_unavailable)
+ flowOf(
+ if (audioStream.value == AudioManager.STREAM_NOTIFICATION) {
+ context.getString(R.string.stream_notification_unavailable)
+ } else {
+ context.getString(R.string.stream_alarm_unavailable)
+ }
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 7ba797c..86063ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -39,7 +39,6 @@
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.media.AudioDeviceAttributes;
@@ -1286,13 +1285,6 @@
}
@Test
- public void setColorFilter_setColorFilterToDrawable() {
- mMediaSwitchingController.setColorFilter(mDrawable, true);
-
- verify(mDrawable).setColorFilter(any(PorterDuffColorFilter.class));
- }
-
- @Test
public void resetGroupMediaDevices_clearGroupDevices() {
final MediaDevice selectedMediaDevice1 = mock(MediaDevice.class);
final MediaDevice selectedMediaDevice2 = mock(MediaDevice.class);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index a0ecb80..f695c13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -76,6 +76,8 @@
@Mock private lateinit var iActivityManager: IActivityManager
+ @Mock private lateinit var beforeUserSwitchingReply: IRemoteCallback
+
@Mock private lateinit var userSwitchingReply: IRemoteCallback
@Mock(stubOnly = true) private lateinit var dumpManager: DumpManager
@@ -199,9 +201,10 @@
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
+ captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
captor.value.onUserSwitching(newID, userSwitchingReply)
runCurrent()
+ verify(beforeUserSwitchingReply).sendResult(any())
verify(userSwitchingReply).sendResult(any())
verify(userManager).getProfiles(newID)
@@ -341,10 +344,11 @@
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
+ captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
captor.value.onUserSwitching(newID, userSwitchingReply)
runCurrent()
+ verify(beforeUserSwitchingReply).sendResult(any())
verify(userSwitchingReply).sendResult(any())
assertThat(callback.calledOnUserChanging).isEqualTo(1)
assertThat(callback.lastUser).isEqualTo(newID)
@@ -395,7 +399,7 @@
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
- captor.value.onBeforeUserSwitching(newID)
+ captor.value.onBeforeUserSwitching(newID, any())
captor.value.onUserSwitchComplete(newID)
runCurrent()
@@ -453,8 +457,10 @@
val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java)
verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString())
+ captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply)
captor.value.onUserSwitching(newID, userSwitchingReply)
runCurrent()
+ verify(beforeUserSwitchingReply).sendResult(any())
verify(userSwitchingReply).sendResult(any())
captor.value.onUserSwitchComplete(newID)
@@ -488,6 +494,7 @@
}
private class TestCallback : UserTracker.Callback {
+ var calledOnBeforeUserChanging = 0
var calledOnUserChanging = 0
var calledOnUserChanged = 0
var calledOnProfilesChanged = 0
@@ -495,6 +502,11 @@
var lastUserContext: Context? = null
var lastUserProfiles = emptyList<UserInfo>()
+ override fun onBeforeUserSwitching(newUser: Int) {
+ calledOnBeforeUserChanging++
+ lastUser = newUser
+ }
+
override fun onUserChanging(newUser: Int, userContext: Context) {
calledOnUserChanging++
lastUser = newUser
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 4cad5f7..77ca51c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -36,7 +36,7 @@
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.RunningChipAnim
import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.ShowingPersistentDot
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.time.FakeSystemClock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index 25138fd..57a12df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -38,6 +38,7 @@
import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
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
@@ -123,7 +124,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testCreateIcons_chipNotifIconFlagEnabled_statusBarChipIconIsNull() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
entry?.let { iconManager.createIcons(it) }
@@ -133,6 +135,17 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
+ entry?.let { iconManager.createIcons(it) }
+ testScope.runCurrent()
+
+ assertThat(entry?.icons?.statusBarChipIcon).isNull()
+ }
+
+ @Test
fun testCreateIcons_importantConversation_shortcutIcon() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true)
@@ -158,7 +171,7 @@
notificationEntry(
hasShortcut = false,
hasMessageSenderIcon = false,
- hasLargeIcon = true
+ hasLargeIcon = true,
)
entry?.channel?.isImportantConversation = true
entry?.let { iconManager.createIcons(it) }
@@ -172,7 +185,7 @@
notificationEntry(
hasShortcut = false,
hasMessageSenderIcon = false,
- hasLargeIcon = false
+ hasLargeIcon = false,
)
entry?.channel?.isImportantConversation = true
entry?.let { iconManager.createIcons(it) }
@@ -187,7 +200,7 @@
hasShortcut = true,
hasMessageSenderIcon = true,
useMessagingStyle = false,
- hasLargeIcon = true
+ hasLargeIcon = true,
)
entry?.channel?.isImportantConversation = true
entry?.let { iconManager.createIcons(it) }
@@ -205,7 +218,8 @@
@Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testCreateIcons_sensitiveImportantConversation() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
entry?.setSensitive(true, true)
@@ -219,8 +233,24 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+ entry?.setSensitive(true, true)
+ entry?.channel?.isImportantConversation = true
+ entry?.let { iconManager.createIcons(it) }
+ testScope.runCurrent()
+ assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+ assertThat(entry?.icons?.statusBarChipIcon).isNull()
+ assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+ assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+ }
+
+ @Test
@EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON)
- fun testUpdateIcons_sensitiveImportantConversation() {
+ @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+ fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
entry?.setSensitive(true, true)
@@ -236,6 +266,23 @@
}
@Test
+ @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME)
+ fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() {
+ val entry =
+ notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
+ entry?.setSensitive(true, true)
+ entry?.channel?.isImportantConversation = true
+ entry?.let { iconManager.createIcons(it) }
+ // Updating the icons after creation shouldn't break anything
+ entry?.let { iconManager.updateIcons(it) }
+ testScope.runCurrent()
+ assertThat(entry?.icons?.statusBarIcon?.sourceIcon).isEqualTo(shortcutIc)
+ assertThat(entry?.icons?.statusBarChipIcon).isNull()
+ assertThat(entry?.icons?.shelfIcon?.sourceIcon).isEqualTo(smallIc)
+ assertThat(entry?.icons?.aodIcon?.sourceIcon).isEqualTo(smallIc)
+ }
+
+ @Test
fun testUpdateIcons_sensitivityChange() {
val entry =
notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false)
@@ -254,7 +301,7 @@
hasShortcut: Boolean,
hasMessageSenderIcon: Boolean,
useMessagingStyle: Boolean = true,
- hasLargeIcon: Boolean
+ hasLargeIcon: Boolean,
): NotificationEntry? {
val n =
Notification.Builder(mContext, "id")
@@ -270,7 +317,7 @@
SystemClock.currentThreadTimeMillis(),
Person.Builder()
.setIcon(if (hasMessageSenderIcon) messageIc else null)
- .build()
+ .build(),
)
)
if (useMessagingStyle) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
index 1709329..2a1877a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
@@ -24,7 +24,6 @@
@Main mainExecutor: Executor,
isUnlocked: Boolean = true,
isShowingAlternateAuthOnUnlock: Boolean = false,
- isPredictiveBackQsDialogAnim: Boolean = false,
interactionJankMonitor: InteractionJankMonitor,
): DialogTransitionAnimator {
return DialogTransitionAnimator(
@@ -35,10 +34,6 @@
isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
),
interactionJankMonitor = interactionJankMonitor,
- featureFlags =
- object : AnimationFeatureFlags {
- override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
- },
transitionAnimator = fakeTransitionAnimator(mainExecutor),
isForTesting = true,
)
@@ -50,6 +45,8 @@
private val isShowingAlternateAuthOnUnlock: Boolean = false,
) : DialogTransitionAnimator.Callback {
override fun isDreaming(): Boolean = isDreaming
+
override fun isUnlocked(): Boolean = isUnlocked
+
override fun isShowingAlternateAuthOnUnlock() = isShowingAlternateAuthOnUnlock
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
index dad8569..9815da9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt
@@ -19,7 +19,6 @@
import android.platform.test.annotations.EnableFlags
import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
-import com.android.systemui.Flags.FLAG_PREDICTIVE_BACK_SYSUI
import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
/**
@@ -29,7 +28,6 @@
@EnableFlags(
FLAG_KEYGUARD_WM_STATE_REFACTOR,
FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN,
- FLAG_PREDICTIVE_BACK_SYSUI,
FLAG_SCENE_CONTAINER,
)
@Retention(AnnotationRetention.RUNTIME)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 47991b3..3df3ee9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -154,6 +154,7 @@
shortcutHelperStateRepository,
activityStarter,
testScope,
+ customInputGesturesRepository
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
index 4383560..1881a94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt
@@ -1,5 +1,6 @@
package com.android.systemui.kosmos
+import androidx.compose.runtime.snapshots.Snapshot
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.FlowValue
import com.android.systemui.coroutines.collectLastValue
@@ -57,6 +58,23 @@
fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) =
testScope.runTest testBody@{ this@runTest.testBody() }
+/**
+ * Runs the given [Kosmos]-scoped test [block] in an environment where compose snapshot state is
+ * settled eagerly. This is the compose equivalent to using an [UnconfinedTestDispatcher] or using
+ * [runCurrent] a lot.
+ *
+ * Note that this shouldn't be needed or used in a compose test environment.
+ */
+fun Kosmos.runTestWithSnapshots(block: suspend Kosmos.() -> Unit) {
+ val handle = Snapshot.registerGlobalWriteObserver { Snapshot.sendApplyNotifications() }
+
+ try {
+ testScope.runTest { block() }
+ } finally {
+ handle.dispose()
+ }
+}
+
fun Kosmos.runCurrent() = testScope.runCurrent()
fun <T> Kosmos.collectLastValue(flow: Flow<T>) = testScope.collectLastValue(flow)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 4a86fd5..74deaab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -21,6 +21,8 @@
import dagger.Binds
import dagger.Module
import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -37,8 +39,8 @@
override val udfpsTransitionToFullShadeProgress =
_udfpsTransitionToFullShadeProgress.asStateFlow()
- private val _currentFling: MutableStateFlow<FlingInfo?> = MutableStateFlow(null)
- override val currentFling: StateFlow<FlingInfo?> = _currentFling.asStateFlow()
+ override val currentFling: MutableSharedFlow<FlingInfo?> =
+ MutableSharedFlow(replay = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val _lockscreenShadeExpansion = MutableStateFlow(0f)
override val lockscreenShadeExpansion = _lockscreenShadeExpansion.asStateFlow()
@@ -139,7 +141,7 @@
}
override fun setCurrentFling(info: FlingInfo?) {
- _currentFling.value = info
+ currentFling.tryEmit(info)
}
override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
index 642c2ff..67f8572 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarContentInsetsProviderStore.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.data.repository
import android.view.Display
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
import org.mockito.kotlin.mock
class FakeStatusBarContentInsetsProviderStore() : StatusBarContentInsetsProviderStore {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
index a34fb09..af7a463 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/StatusBarContentInsetsProviderStoreKosmos.kt
@@ -21,7 +21,7 @@
import com.android.systemui.display.data.repository.displayWindowPropertiesRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.statusbar.phone.statusBarContentInsetsProviderFactory
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProviderFactory
import com.android.systemui.sysUICutoutProviderFactory
val Kosmos.fakeStatusBarContentInsetsProviderStore by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
similarity index 95%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
index 4fb8cf4..ad742c8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/FakeStatusBarContentInsetsProviderFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/FakeStatusBarContentInsetsProviderFactory.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import android.content.Context
import com.android.systemui.SysUICutoutProvider
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
similarity index 91%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
index 705df3c..69e215d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.phone
+package com.android.systemui.statusbar.layout
import com.android.systemui.kosmos.Kosmos
import org.mockito.kotlin.mock
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
index 7eaecb1..3a19547 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
@@ -19,7 +19,7 @@
import android.content.Context
import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController
-import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider
class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory {
override fun create(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
index 23f2b42..f595aef 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -22,7 +22,7 @@
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.fragments.fragmentService
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.phone.statusBarContentInsetsProvider
+import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider
import com.android.systemui.statusbar.policy.statusBarConfigurationController
import java.util.Optional
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
new file mode 100644
index 0000000..ca02576
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+/** Returns a [State] that is `true` only when all of [states] are `true`. */
+@ExperimentalKairosApi
+fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
+
+/** Returns a [State] that is `true` when any of [states] are `true`. */
+@ExperimentalKairosApi
+fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
+
+/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
+@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
+
+private fun Iterable<Boolean>.allTrue() = all { it }
+
+private fun Iterable<Boolean>.anyTrue() = any { it }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
index b691870..bd2173c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
@@ -17,17 +17,14 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -36,9 +33,8 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.launch
-/** A function that modifies the KairosNetwork. */
+/** A computation that can modify the Kairos network. */
typealias BuildSpec<A> = BuildScope.() -> A
/**
@@ -56,17 +52,7 @@
/** Operations that add inputs and outputs to a Kairos network. */
@ExperimentalKairosApi
-interface BuildScope : StateScope {
-
- /**
- * A [KairosNetwork] handle that is bound to this [BuildScope].
- *
- * It supports all of the standard functionality by which external code can interact with this
- * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
- * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children
- * are also destroyed.
- */
- val kairosNetwork: KairosNetwork
+interface BuildScope : HasNetwork, StateScope {
/**
* Defers invoking [block] until after the current [BuildScope] code-path completes, returning a
@@ -110,11 +96,21 @@
* executed if this [BuildScope] is still active by that time. It can be deactivated due to a
* -Latest combinator, for example.
*
- * Shorthand for:
- * ```kotlin
- * events.observe { effect { ... } }
+ * [Disposing][DisposableHandle.dispose] of the returned [DisposableHandle] will stop the
+ * observation of new emissions. It will however *not* cancel any running effects from previous
+ * emissions. To achieve this behavior, use [launchScope] or [asyncScope] to create a child
+ * build scope:
+ * ``` kotlin
+ * val job = launchScope {
+ * events.observe { x ->
+ * launchEffect { longRunningEffect(x) }
+ * }
+ * }
+ * // cancels observer and any running effects:
+ * job.cancel()
* ```
*/
+ // TODO: remove disposable handle return? might add more confusion than convenience
fun <A> Events<A>.observe(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: EffectScope.(A) -> Unit = {},
@@ -129,7 +125,7 @@
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -138,32 +134,32 @@
): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
/**
- * Creates an instance of an [Events] with elements that are from [builder].
+ * Creates an instance of an [Events] with elements that are emitted from [builder].
*
* [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableState].
+ * provided [EventProducerScope].
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * events { ... }.apply { observe() }
+ * ``` kotlin
+ * events { ... }.apply { observe() }
* ```
*/
- fun <T> events(
- name: String? = null,
- builder: suspend EventProducerScope<T>.() -> Unit,
- ): Events<T>
+ // TODO: eventually this should be defined on KairosNetwork + an extension on HasNetwork
+ // - will require modifying InputNode so that it can be manually killed, as opposed to using
+ // takeUntil (which requires a StateScope).
+ fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T>
/**
* Creates an instance of an [Events] with elements that are emitted from [builder].
*
* [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableState].
+ * provided [CoalescingEventProducerScope].
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * events { ... }.apply { observe() }
+ * ``` kotlin
+ * events { ... }.apply { observe() }
* ```
*
* In the event of backpressure, emissions are *coalesced* into batches. When a value is
@@ -171,6 +167,7 @@
* [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the
* batch is reset back to [getInitialValue].
*/
+ // TODO: see TODO for [events]
fun <In, Out> coalescingEvents(
getInitialValue: () -> Out,
coalesce: (old: Out, new: In) -> Out,
@@ -186,6 +183,7 @@
*
* The return value from [block] can be accessed via the returned [DeferredValue].
*/
+ // TODO: return a DisposableHandle instead of Job?
fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job>
// TODO: once we have context params, these can all become extensions:
@@ -198,9 +196,9 @@
* outside of the current Kairos transaction; when [transform] returns, the returned value is
* emitted from the result [Events] in a new transaction.
*
- * Shorthand for:
- * ```kotlin
- * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
+ * mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
* ```
*/
fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
@@ -219,42 +217,19 @@
/**
* Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
* [value of this State][State.sample], and will emit at the same rate as [State.changes].
- *
- * Note that the [value][StateFlow.value] is not available until the *end* of the current
- * transaction. If you need the current value before this time, then use [State.sample].
*/
fun <A> State<A>.toStateFlow(): StateFlow<A> {
- val uninitialized = Any()
- var initialValue: Any? = uninitialized
- val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
- deferredBuildScope {
- initialValue = sample()
- changes.observe {
- innerStateFlow.value = it
- initialValue = null
- }
- }
-
- @Suppress("UNCHECKED_CAST")
- fun getValue(innerValue: Any?): A =
- when {
- innerValue !== uninitialized -> innerValue as A
- initialValue !== uninitialized -> initialValue as A
- else ->
- error(
- "Attempted to access StateFlow.value before Kairos transaction has completed."
- )
- }
-
+ val innerStateFlow = MutableStateFlow(sampleDeferred())
+ changes.observe { innerStateFlow.value = deferredOf(it) }
return object : StateFlow<A> {
override val replayCache: List<A>
- get() = innerStateFlow.replayCache.map(::getValue)
+ get() = innerStateFlow.replayCache.map { it.value }
override val value: A
- get() = getValue(innerStateFlow.value)
+ get() = innerStateFlow.value.value
override suspend fun collect(collector: FlowCollector<A>): Nothing {
- innerStateFlow.collect { collector.emit(getValue(it)) }
+ innerStateFlow.collect { collector.emit(it.value) }
}
}
}
@@ -365,14 +340,14 @@
initialSpec: BuildSpec<A>
): Pair<Events<B>, DeferredValue<A>> {
val (events, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
+ mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
.applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
val outEvents: Events<B> =
events.mapMaybe {
checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
}
val outInit: DeferredValue<A> = deferredBuildScope {
- val initResult: Map<Unit, A> = result.get()
+ val initResult: Map<Unit, A> = result.value
check(Unit in initResult) {
"applyLatest: expected initial result, but none present in: $initResult"
}
@@ -425,7 +400,7 @@
transform: BuildScope.(A) -> B,
): Pair<Events<B>, DeferredValue<B>> =
mapCheap { buildSpec { transform(it) } }
- .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) })
+ .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.value) })
/**
* Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
@@ -436,7 +411,7 @@
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -445,6 +420,17 @@
): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+ /**
+ * Returns an [Incremental] containing the results of applying each [BuildSpec] emitted from the
+ * original [Incremental].
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the
+ * same key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
fun <K, V> Incremental<K, BuildSpec<V>>.applyLatestSpecForKey(
numKeys: Int? = null
): Incremental<K, V> {
@@ -460,7 +446,7 @@
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.applyLatestSpecForKey(
@@ -476,7 +462,7 @@
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -495,7 +481,7 @@
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -513,7 +499,7 @@
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -524,7 +510,7 @@
map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } }
.applyLatestSpecForKey(
deferredBuildScope {
- initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } }
+ initialValues.value.mapValues { (k, v) -> buildSpec { transform(k, v) } }
},
numKeys = numKeys,
)
@@ -539,7 +525,7 @@
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -558,7 +544,7 @@
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -570,7 +556,7 @@
fun <R> Events<R>.nextDeferred(): Deferred<R> {
lateinit var next: CompletableDeferred<R>
val job = launchScope { nextOnly().observe { next.complete(it) } }
- next = CompletableDeferred<R>(parent = job)
+ next = CompletableDeferred(parent = job)
return next
}
@@ -581,12 +567,11 @@
}
/** Returns an [Events] that emits whenever this [Flow] emits. */
- fun <A> Flow<A>.toEvents(name: String? = null): Events<A> =
- events(name) { collect { emit(it) } }
+ fun <A> Flow<A>.toEvents(): Events<A> = events { collect { emit(it) } }
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.toEvents().holdState(initialValue)
* ```
*/
@@ -594,7 +579,7 @@
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.scan(initialValue, operation).toEvents().holdState(initialValue)
* ```
*/
@@ -603,7 +588,7 @@
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue)
* ```
*/
@@ -679,6 +664,13 @@
* Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can
* be used to make further modifications to the Kairos network, and/or perform side-effects via
* [effect].
+ *
+ * ``` kotlin
+ * fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
+ * block(sample())
+ * changes.observeBuild(block)
+ * }
+ * ```
*/
fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
block(sample())
@@ -706,12 +698,9 @@
* outside of the current Kairos transaction; when it completes, the returned [Events] emits in a
* new transaction.
*
- * Shorthand for:
- * ```
- * events { emitter: MutableEvents<A> ->
- * val a = block()
- * emitter.emit(a)
- * }
+ * ``` kotlin
+ * fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
+ * events { emit(block()) }.apply { observe() }
* ```
*/
@ExperimentalKairosApi
@@ -730,9 +719,12 @@
* executed if this [BuildScope] is still active by that time. It can be deactivated due to a
* -Latest combinator, for example.
*
- * Shorthand for:
- * ```kotlin
- * launchScope { now.observe { block() } }
+ * ``` kotlin
+ * fun BuildScope.effect(
+ * context: CoroutineContext = EmptyCoroutineContext,
+ * block: EffectScope.() -> Unit,
+ * ): Job =
+ * launchScope { now.observe(context) { block() } }
* ```
*/
@ExperimentalKairosApi
@@ -748,13 +740,14 @@
* done because the current [BuildScope] might be deactivated within this transaction, perhaps due
* to a -Latest combinator. If this happens, then the coroutine will never actually be started.
*
- * Shorthand for:
- * ```kotlin
- * effect { effectCoroutineScope.launch { block() } }
+ * ``` kotlin
+ * fun BuildScope.launchEffect(block: suspend KairosScope.() -> Unit): Job =
+ * effect { effectCoroutineScope.launch { block() } }
* ```
*/
@ExperimentalKairosApi
-fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+fun BuildScope.launchEffect(block: suspend KairosCoroutineScope.() -> Unit): Job =
+ asyncEffect(block)
/**
* Launches [block] in a new coroutine, returning the result as a [Deferred].
@@ -764,17 +757,18 @@
* to a -Latest combinator. If this happens, then the coroutine will never actually be started.
*
* Shorthand for:
- * ```kotlin
- * CompletableDeferred<R>.apply {
- * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } }
- * }
- * .await()
+ * ``` kotlin
+ * fun <R> BuildScope.asyncEffect(block: suspend KairosScope.() -> R): Deferred<R> =
+ * CompletableDeferred<R>.apply {
+ * effect { effectCoroutineScope.launch { complete(block()) } }
+ * }
+ * .await()
* ```
*/
@ExperimentalKairosApi
-fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+fun <R> BuildScope.asyncEffect(block: suspend KairosCoroutineScope.() -> R): Deferred<R> {
val result = CompletableDeferred<R>()
- val job = effect { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+ val job = effect { launch { result.complete(block()) } }
val handle = job.invokeOnCompletion { result.cancel() }
result.invokeOnCompletion {
handle.dispose()
@@ -795,7 +789,7 @@
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
* events { ... }.apply { observe() }
* ```
*
@@ -819,7 +813,7 @@
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
* events { ... }.apply { observe() }
* ```
*
@@ -837,7 +831,7 @@
}
/** Scope for emitting to a [BuildScope.coalescingEvents]. */
-interface CoalescingEventProducerScope<in T> {
+fun interface CoalescingEventProducerScope<in T> {
/**
* Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
* already pending.
@@ -850,7 +844,7 @@
}
/** Scope for emitting to a [BuildScope.events]. */
-interface EventProducerScope<in T> {
+fun interface EventProducerScope<in T> {
/**
* Emits a [value] to this [Events], suspending the caller until the Kairos transaction
* containing the emission has completed.
@@ -868,3 +862,11 @@
} finally {
block()
}
+
+/**
+ * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
+ * [State] that holds the result of the currently-active [BuildSpec].
+ */
+@ExperimentalKairosApi
+fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
+ rebuildSignal.map { spec }.holdLatestSpec(spec)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
deleted file mode 100644
index c208646..0000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * 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.kairos
-
-import com.android.systemui.kairos.util.These
-import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.channelFlow
-import kotlinx.coroutines.flow.conflate
-
-/**
- * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
- * emission of the original [Events], within the same transaction of the original emission.
- */
-@ExperimentalKairosApi
-fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
- state: State<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, state.sample()) }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
- sampleable: Transactional<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, sampleable.sample()) }
-
-/**
- * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
- * the new value is passed to [transform].
- *
- * Note that [sample] is both more performant, and safer to use with recursive definitions. You will
- * generally want to use it rather than this.
- *
- * @see sample
- */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.samplePromptly(
- state: State<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> =
- sample(state) { a, b -> These.thiz(a to b) }
- .mergeWith(state.changes.map { These.that(it) }) { thiz, that ->
- These.both((thiz as These.This).thiz, (that as These.That).that)
- }
- .mapMaybe { these ->
- when (these) {
- // both present, transform the upstream value and the new value
- is These.Both -> just(transform(these.thiz.first, these.that))
- // no upstream present, so don't perform the sample
- is These.That -> none()
- // just the upstream, so transform the upstream and the old value
- is These.This -> just(transform(these.thiz.first, these.thiz.second))
- }
- }
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
- * transactionally connect to / disconnect from the [Events] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
- * transactionally connect to / disconnect from the [State] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("eventsSpecToColdConflatedFlow")
-fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("stateSpecToColdConflatedFlow")
-fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [Events].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalFlowToColdConflatedFlow")
-fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalStateToColdConflatedFlow")
-fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulFlowToColdConflatedFlow")
-fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulStateToColdConflatedFlow")
-fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
-
-private fun Iterable<Boolean>.allTrue() = all { it }
-
-private fun Iterable<Boolean>.anyTrue() = any { it }
-
-/** Returns a [State] that is `true` only when all of [states] are `true`. */
-@ExperimentalKairosApi
-fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
-
-/** Returns a [State] that is `true` when any of [states] are `true`. */
-@ExperimentalKairosApi
-fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
-
-/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
-@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
- * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
- * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
- * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
- *
- * Use [compiledBuildSpec] to compile and stand-up a mode graph.
- *
- * @see StatefulMode
- */
-@ExperimentalKairosApi
-fun interface BuildMode<out A> {
- /**
- * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
- * new mode.
- */
- fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
-}
-
-/**
- * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
- * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
- get() = buildSpec {
- var modeChangeEvents by EventsLoop<BuildMode<A>>()
- val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
- modeChangeEvents
- .map { it.run { buildSpec { enableMode() } } }
- .holdLatestSpec(buildSpec { enableMode() })
- modeChangeEvents =
- activeMode
- .map { statefully { it.second.nextOnly() } }
- .applyLatestStateful()
- .switchEvents()
- activeMode.map { it.first }
- }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all state accumulation is immediately started. When the returned
- * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
- * accumulation in the process.
- *
- * Use [compiledStateful] to compile and stand-up a mode graph.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-fun interface StatefulMode<out A> {
- /**
- * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
- * new mode.
- */
- fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
-}
-
-/**
- * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a
- * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as
- * they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
- get() = statefully {
- var modeChangeEvents by EventsLoop<StatefulMode<A>>()
- val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
- modeChangeEvents
- .map { it.run { statefully { enableMode() } } }
- .holdLatestStateful(statefully { enableMode() })
- modeChangeEvents =
- activeMode
- .map { statefully { it.second.nextOnly() } }
- .applyLatestStateful()
- .switchEvents()
- activeMode.map { it.first }
- }
-
-/**
- * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
- * [State] that holds the result of the currently-active [BuildSpec].
- */
-@ExperimentalKairosApi
-fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
- rebuildSignal.map { spec }.holdLatestSpec(spec)
-
-/**
- * Like [changes] but also includes the old value of this [State].
- *
- * Shorthand for:
- * ``` kotlin
- * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
- * ```
- */
-@ExperimentalKairosApi
-val <A> State<A>.transitions: Events<WithPrev<A, A>>
- get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
new file mode 100644
index 0000000..b3d89c3
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.zipStates
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.combineState
+ */
+@ExperimentalKairosApi
+@JvmName(name = "stateCombine")
+fun <A, B, C> State<A>.combine(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
+ combine(this, other, transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<State<A>>.combine(): State<List<A>> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ val states = map { it.init }
+ zipStates(
+ name,
+ operatorName,
+ states.size,
+ states = init(null) { states.map { it.connect(this) } },
+ )
+ }
+ )
+}
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> =
+ asIterable().map { (k, state) -> state.map { v -> k to v } }.combine().map { it.toMap() }
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
+ combine().map(transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [State]s into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
+ states.asIterable().combine(transform)
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ transform: KairosScope.(A, B) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
+ NoScope.transform(a, b)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ transform: KairosScope.(A, B, C) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
+ NoScope.transform(a, b, c)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ transform: KairosScope.(A, B, C, D) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
+ a,
+ b,
+ c,
+ d ->
+ NoScope.transform(a, b, c, d)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, E, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ stateE: State<E>,
+ transform: KairosScope.(A, B, C, D, E) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(
+ name,
+ operatorName,
+ stateA.init,
+ stateB.init,
+ stateC.init,
+ stateD.init,
+ stateE.init,
+ ) { a, b, c, d, e ->
+ NoScope.transform(a, b, c, d, e)
+ }
+ }
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
new file mode 100644
index 0000000..4b9bb0e
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.CompletableLazy
+
+/**
+ * A value that may not be immediately (synchronously) available, but is guaranteed to be available
+ * before this transaction is completed.
+ */
+@ExperimentalKairosApi
+class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) {
+ /**
+ * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is
+ * not yet available.
+ */
+ val value: A
+ get() = unwrapped.value
+}
+
+/** Returns an already-available [DeferredValue] containing [value]. */
+@ExperimentalKairosApi
+fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
index 7e257f2..14d45d4 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
@@ -16,33 +16,46 @@
package com.android.systemui.kairos
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
/**
- * Scope for external side-effects triggered by the Kairos network. This still occurs within the
- * context of a transaction, so general suspending calls are disallowed to prevent blocking the
- * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new
- * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the
- * containing [BuildScope] that this side-effect scope is running in.
+ * Scope for external side-effects triggered by the Kairos network.
+ *
+ * This still occurs within the context of a transaction, so general suspending calls are disallowed
+ * to prevent blocking the transaction. You can [launch] new coroutines to perform long-running
+ * asynchronous work. These coroutines are kept alive for the duration of the containing
+ * [BuildScope] that this side-effect scope is running in.
*/
@ExperimentalKairosApi
-interface EffectScope : TransactionScope {
+interface EffectScope : HasNetwork, TransactionScope {
/**
- * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is
- * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is
- * cancelled.
+ * Creates a coroutine that is a child of this [EffectScope], and returns its future result as a
+ * [Deferred].
+ *
+ * @see kotlinx.coroutines.async
*/
- @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope
+ fun <R> async(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend KairosCoroutineScope.() -> R,
+ ): Deferred<R>
/**
- * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos
- * network.
+ * Launches a new coroutine that is a child of this [EffectScope] without blocking the current
+ * thread and returns a reference to the coroutine as a [Job].
*
- * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope]
- * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any
- * modifications to the Kairos network performed via this [KairosNetwork] instance will be
- * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending
- * [side-effects][BuildScope.effect] are cancelled).
+ * @see kotlinx.coroutines.launch
*/
- @ExperimentalKairosApi val kairosNetwork: KairosNetwork
+ fun launch(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend KairosCoroutineScope.() -> Unit,
+ ): Job = async(context, start, block)
}
+
+@ExperimentalKairosApi interface KairosCoroutineScope : HasNetwork, CoroutineScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
index e7d0096..8f468c1 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
@@ -17,7 +17,6 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DemuxImpl
import com.android.systemui.kairos.internal.EventsImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
@@ -27,22 +26,11 @@
import com.android.systemui.kairos.internal.activated
import com.android.systemui.kairos.internal.cached
import com.android.systemui.kairos.internal.constInit
-import com.android.systemui.kairos.internal.demuxMap
-import com.android.systemui.kairos.internal.filterImpl
-import com.android.systemui.kairos.internal.filterJustImpl
import com.android.systemui.kairos.internal.init
import com.android.systemui.kairos.internal.mapImpl
-import com.android.systemui.kairos.internal.mergeNodes
-import com.android.systemui.kairos.internal.mergeNodesLeft
import com.android.systemui.kairos.internal.neverImpl
-import com.android.systemui.kairos.internal.switchDeferredImplSingle
-import com.android.systemui.kairos.internal.switchPromptImplSingle
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.toMaybe
import java.util.concurrent.atomic.AtomicReference
import kotlin.reflect.KProperty
@@ -51,7 +39,16 @@
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
-/** A series of values of type [A] available at discrete points in time. */
+/**
+ * A series of values of type [A] available at discrete points in time.
+ *
+ * [Events] follow these rules:
+ * 1. Within a single Kairos network transaction, an [Events] instance will only emit *once*.
+ * 2. The order that different [Events] instances emit values within a transaction is undefined, and
+ * are conceptually *simultaneous*.
+ * 3. [Events] emissions are *ephemeral* and do not last beyond the transaction they are emitted,
+ * unless explicitly [observed][BuildScope.observe] or [held][StateScope.holdState] as a [State].
+ */
@ExperimentalKairosApi
sealed class Events<out A> {
companion object {
@@ -67,7 +64,9 @@
* A forward-reference to an [Events]. Useful for recursive definitions.
*
* This reference can be used like a standard [Events], but will throw an error if its [loopback] is
- * unset before the end of the first transaction which accesses it.
+ * unset before it is [observed][BuildScope.observe].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.eventsLoop
*/
@ExperimentalKairosApi
class EventsLoop<A> : Events<A>() {
@@ -76,7 +75,10 @@
internal val init: Init<EventsImpl<A>> =
init(name = null) { deferred.value.init.connect(evalScope = this) }
- /** The [Events] this reference is referring to. */
+ /**
+ * The [Events] this reference is referring to. Must be set before this [EventsLoop] is
+ * [observed][BuildScope.observe].
+ */
var loopback: Events<A>? = null
set(value) {
value?.let {
@@ -102,6 +104,12 @@
* will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Events<A>>.defer() = deferredEvents { value }
+ * ```
+ *
+ * @see deferredEvents
*/
@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value }
@@ -113,6 +121,12 @@
* and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() }
+ * ```
+ *
+ * @see deferredEvents
*/
@ExperimentalKairosApi
fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value }
@@ -130,25 +144,27 @@
NoScope.block()
}
-/** Returns an [Events] that emits the new value of this [State] when it changes. */
-@ExperimentalKairosApi
-val <A> State<A>.changes: Events<A>
- get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
-
/**
- * Returns an [Events] that contains only the [just] results of applying [transform] to each value
- * of the original [Events].
+ * Returns an [Events] that contains only the
+ * [present][com.android.systemui.kairos.util.Maybe.present] results of applying [transform] to each
+ * value of the original [Events].
*
+ * @sample com.android.systemui.kairos.KairosSamples.mapMaybe
* @see mapNotNull
*/
@ExperimentalKairosApi
fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> =
- map(transform).filterJust()
+ map(transform).filterPresent()
/**
* Returns an [Events] that contains only the non-null results of applying [transform] to each value
* of the original [Events].
*
+ * ``` kotlin
+ * fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> =
+ * mapMaybe { if (it == null) absent else present(it) }
+ * ```
+ *
* @see mapMaybe
*/
@ExperimentalKairosApi
@@ -156,23 +172,11 @@
transform(it).toMaybe()
}
-/** Returns an [Events] containing only values of the original [Events] that are not null. */
-@ExperimentalKairosApi
-fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust()
-
-/** Shorthand for `mapNotNull { it as? A }`. */
-@ExperimentalKairosApi
-inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
- mapCheap { it as? A }.filterNotNull()
-
-/** Shorthand for `mapMaybe { it }`. */
-@ExperimentalKairosApi
-fun <A> Events<Maybe<A>>.filterJust(): Events<A> =
- EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) }))
-
/**
* Returns an [Events] containing the results of applying [transform] to each value of the original
* [Events].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapEvents
*/
@ExperimentalKairosApi
fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
@@ -184,6 +188,7 @@
* Like [map], but the emission is not cached during the transaction. Use only if [transform] is
* fast and pure.
*
+ * @sample com.android.systemui.kairos.KairosSamples.mapCheap
* @see map
*/
@ExperimentalKairosApi
@@ -196,8 +201,9 @@
* Returns an [Events] that invokes [action] before each value of the original [Events] is emitted.
* Useful for logging and debugging.
*
- * ```
- * pulse.onEach { foo(it) } == pulse.map { foo(it); it }
+ * ``` kotlin
+ * fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> =
+ * map { it.also { action(it) } }
* ```
*
* Note that the side effects performed in [onEach] are only performed while the resulting [Events]
@@ -207,29 +213,19 @@
*/
@ExperimentalKairosApi
fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map {
- action(it)
- it
-}
-
-/**
- * Returns an [Events] containing only values of the original [Events] that satisfy the given
- * [predicate].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
- val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
- return EventsInit(constInit(name = null, pulse))
+ it.also { action(it) }
}
/**
* Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of
* the original.
*
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ * fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
+ * val lefts = map { it.first }
+ * val rights = map { it.second }
+ * return lefts to rights
+ * }
* ```
*/
@ExperimentalKairosApi
@@ -240,246 +236,6 @@
}
/**
- * Merges the given [Events] into a single [Events] that emits events from both.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.mergeWith(
- other: Events<A>,
- name: String? = null,
- transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
-): Events<A> {
- val node =
- mergeNodes(
- name = name,
- getPulse = { init.connect(evalScope = this) },
- getOther = { other.init.connect(evalScope = this) },
- ) { a, b ->
- transformCoincidence(a, b)
- }
- return EventsInit(constInit(name = null, node))
-}
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-// TODO: can be optimized to avoid creating the intermediate list
-fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
- merge(*events).map { l -> l.reduce(transformCoincidence) }
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
- EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
- EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [Map], and are given the same key of the associated [Events] in the
- * input [Map].
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
- asSequence()
- .map { (k, events) -> events.map { a -> k to a } }
- .toList()
- .merge()
- .map { it.toMap() }
-
-/**
- * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
- * downstream [Events].
- *
- * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
- * value will be emitted from. These downstream [Events] can be obtained via
- * [GroupedEvents.eventsForKey].
- *
- * An example:
- * ```
- * val fooEvents: Events<Map<String, Foo>> = ...
- * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
- * val fooBar: Events<Foo> = fooById["bar"]
- * ```
- *
- * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
- * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
- * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
- * the appropriate downstream [Events], and so operates in `O(1)`.
- *
- * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see selector
- */
-@ExperimentalKairosApi
-fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
- GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
-
-/**
- * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-fun <K, A> Events<A>.groupBy(
- numKeys: Int? = null,
- extractKey: TransactionScope.(A) -> K,
-): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
-
-/**
- * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
- * [predicate].
- *
- * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
- * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.partition(
- predicate: TransactionScope.(A) -> Boolean
-): Pair<Events<A>, Events<A>> {
- val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
- return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
-}
-
-/**
- * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
- * [Left] values, and [Pair.second] will contain [Right] values.
- *
- * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
- * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the
- * [filterIsInstance] check is only performed once per element.
- */
-@ExperimentalKairosApi
-fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
- val (left, right) = partition { it is Left }
- return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value })
-}
-
-/**
- * A mapping from keys of type [K] to [Events] emitting values of type [A].
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
- /**
- * Returns an [Events] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
-
- /**
- * Returns an [Events] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- operator fun get(key: K): Events<A> = eventsForKey(key)
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch does take effect until the *next* transaction after [State] changes. For a switch
- * that takes effect immediately, see [switchEventsPromptly].
- */
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
- return EventsInit(
- constInit(
- name = null,
- switchDeferredImplSingle(
- name = name,
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch takes effect immediately within the same transaction that [State] changes. In
- * general, you should prefer [switchEvents] over this method. It is both safer and more performant.
- */
-// TODO: parameter to handle coincidental emission from both old and new
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
- return EventsInit(
- constInit(
- name = null,
- switchPromptImplSingle(
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
* A mutable [Events] that provides the ability to [emit] values to the network, handling
* backpressure by coalescing all emissions into batches.
*
@@ -494,7 +250,7 @@
private val getInitialValue: () -> Out,
internal val impl: InputNode<Out> = InputNode(),
) : Events<Out>() {
- internal val storage = AtomicReference(false to lazy { getInitialValue() })
+ private val storage = AtomicReference(false to lazy { getInitialValue() })
override fun toString(): String = "${this::class.simpleName}@$hashString"
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
new file mode 100644
index 0000000..8ca5ac8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.filterImpl
+import com.android.systemui.kairos.internal.filterPresentImpl
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.toMaybe
+
+/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are not null.
+ *
+ * ``` kotlin
+ * fun <A> Events<A?>.filterNotNull(): Events<A> = mapNotNull { it }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterPresent()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are instances of [A].
+ *
+ * ``` kotlin
+ * inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ * mapNotNull { it as? A }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ mapCheap { it as? A }.filterNotNull()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are present.
+ *
+ * ``` kotlin
+ * fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = mapMaybe { it }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<Maybe<A>>.filterPresent(): Events<A> =
+ EventsInit(constInit(name = null, filterPresentImpl { init.connect(evalScope = this) }))
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that satisfy the given
+ * [predicate].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ * mapMaybe { if (predicate(it)) present(it) else absent }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
+ val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
+ return EventsInit(constInit(name = null, pulse))
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
new file mode 100644
index 0000000..45da34a
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.DemuxImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.demuxMap
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
+import com.android.systemui.kairos.util.orError
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events].
+ *
+ * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
+ * value will be emitted from. These downstream [Events] can be obtained via
+ * [GroupedEvents.eventsForKey].
+ *
+ * An example:
+ * ```
+ * val fooEvents: Events<Map<String, Foo>> = ...
+ * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
+ * val fooBar: Events<Foo> = fooById["bar"]
+ * ```
+ *
+ * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
+ * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
+ * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
+ * the appropriate downstream [Events], and so operates in `O(1)`.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal [HashMap].
+ *
+ * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.groupByKey
+ * @see selector
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
+ GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events]. The downstream [Events] are associated with a [key][K], which is derived
+ * from each emission of the original [Events] via [extractKey].
+ *
+ * ``` kotlin
+ * fun <K, A> Events<A>.groupBy(
+ * numKeys: Int? = null,
+ * extractKey: TransactionScope.(A) -> K,
+ * ): GroupedEvents<K, A> =
+ * map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+ * ```
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<A>.groupBy(
+ numKeys: Int? = null,
+ extractKey: TransactionScope.(A) -> K,
+): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+
+/**
+ * A mapping from keys of type [K] to [Events] emitting values of type [A].
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
+ /**
+ * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
+
+ /**
+ * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ operator fun get(key: K): Events<A> = eventsForKey(key)
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
+ * [predicate].
+ *
+ * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
+ * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.partition(
+ * predicate: TransactionScope.(A) -> Boolean
+ * ): Pair<Events<A>, Events<A>> =
+ * map { if (predicate(it)) left(it) else right(it) }.partitionEither()
+ * ```
+ *
+ * @see partitionEither
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.partition(
+ predicate: TransactionScope.(A) -> Boolean
+): Pair<Events<A>, Events<A>> {
+ val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
+ return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [First] values, and [Pair.second] will contain [Second] values.
+ *
+ * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
+ * [First]s and once for [Second]s, but is slightly more efficient; specifically, the
+ * [filterIsInstance] check is only performed once per element.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> =
+ * map { it.asThese() }.partitionThese()
+ * ```
+ *
+ * @see partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
+ val (left, right) = partition { it is Either.First }
+ return Pair(
+ left.mapCheap { (it as Either.First).value },
+ right.mapCheap { (it as Either.Second).value },
+ )
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [These.first] values, and [Pair.second] will contain [These.second] values. If the original
+ * emission was a [These.both], then both result [Events] will emit a value simultaneously.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<These<A, B>>.partitionThese(): Pair<Events<A>, Events<B>> {
+ val grouped =
+ mapCheap {
+ when (it) {
+ is These.Both -> mapOf(true to it, false to it)
+ is These.Second -> mapOf(false to it)
+ is These.First -> mapOf(true to it)
+ }
+ }
+ .groupByKey(numKeys = 2)
+ return Pair(
+ grouped.eventsForKey(true).mapCheap {
+ it.maybeFirst().orError { "unexpected missing value" }
+ },
+ grouped.eventsForKey(false).mapCheap {
+ it.maybeSecond().orError { "unexpected missing value" }
+ },
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
index c95b9e8..d88ae3b8 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
@@ -21,27 +21,31 @@
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
import com.android.systemui.kairos.internal.NoScope
-import com.android.systemui.kairos.internal.awaitValues
import com.android.systemui.kairos.internal.constIncremental
import com.android.systemui.kairos.internal.constInit
import com.android.systemui.kairos.internal.init
-import com.android.systemui.kairos.internal.mapImpl
import com.android.systemui.kairos.internal.mapValuesImpl
-import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
-import com.android.systemui.kairos.internal.switchDeferredImpl
-import com.android.systemui.kairos.internal.switchPromptImpl
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.util.MapPatch
-import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.mapPatchFromFullDiff
import kotlin.reflect.KProperty
-/** A [State] tracking a [Map] that receives incremental updates. */
+/**
+ * A [State] tracking a [Map] that receives incremental updates.
+ *
+ * [Incremental] allows one to react to the [subset of changes][updates] to the held map, without
+ * having to perform a manual diff of the map to determine what changed.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ */
sealed class Incremental<K, out V> : State<Map<K, V>>() {
abstract override val init: Init<IncrementalImpl<K, V>>
}
-/** An [Incremental] that never changes. */
+/**
+ * Returns a constant [Incremental] that never changes. [changes] and [updates] are both equivalent
+ * to [emptyEvents], and [TransactionScope.sample] will always produce [value].
+ */
@ExperimentalKairosApi
fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> {
val operatorName = "stateOf"
@@ -57,6 +61,10 @@
* [value][Lazy.value] will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Incremental<K, V>>.defer() = deferredIncremental { value }
+ * ```
*/
@ExperimentalKairosApi
fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { value }
@@ -69,6 +77,10 @@
* queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Incremental<K, V>>.defer() = deferredIncremental { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <K, V> DeferredValue<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline {
@@ -119,94 +131,14 @@
}
/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
+ * A forward-reference to an [Incremental]. Useful for recursive definitions.
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
- * map { it.merge() }.switchEvents()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
+ * This reference can be used like a standard [Incremental], but will throw an error if its
+ * [loopback] is unset before it is [observed][BuildScope.observe] or
+ * [sampled][TransactionScope.sample]. Note that it is safe to invoke
+ * [TransactionScope.sampleDeferred] before [loopback] is set, provided the [DeferredValue] is not
+ * [queried][KairosScope.get].
*/
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
- val operatorName = "mergeEventsIncrementally"
- val name = operatorName
- val patches =
- mapImpl({ init.connect(this).patches }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
- }
- return EventsInit(
- constInit(
- name,
- switchDeferredImpl(
- name = name,
- getStorage = {
- init
- .connect(this)
- .getCurrentWithEpoch(this)
- .first
- .mapValues { (_, events) -> events.init.connect(this) }
- .asIterable()
- },
- getPatches = { patches },
- storeFactory = ConcurrentHashMapK.Factory(),
- )
- .awaitValues(),
- )
- )
-}
-
-/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
- * map { it.merge() }.switchEventsPromptly()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
- val operatorName = "mergeEventsIncrementally"
- val name = operatorName
- val patches =
- mapImpl({ init.connect(this).patches }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
- }
- return EventsInit(
- constInit(
- name,
- switchPromptImpl(
- name = name,
- getStorage = {
- init
- .connect(this)
- .getCurrentWithEpoch(this)
- .first
- .mapValues { (_, events) -> events.init.connect(this) }
- .asIterable()
- },
- getPatches = { patches },
- storeFactory = ConcurrentHashMapK.Factory(),
- )
- .awaitValues(),
- )
- )
-}
-
-/** A forward-reference to an [Incremental], allowing for recursive definitions. */
@ExperimentalKairosApi
class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V>() {
@@ -215,7 +147,10 @@
override val init: Init<IncrementalImpl<K, V>> =
init(name) { deferred.value.init.connect(evalScope = this) }
- /** The [Incremental] this [IncrementalLoop] will forward to. */
+ /**
+ * The [Incremental] this reference is referring to. Must be set before this [IncrementalLoop]
+ * is [observed][BuildScope.observe].
+ */
var loopback: Incremental<K, V>? = null
set(value) {
value?.let {
@@ -237,8 +172,8 @@
}
/**
- * Returns an [Incremental] whose [updates] are calculated by diffing the given [State]'s
- * [transitions].
+ * Returns an [Incremental] whose [updates] are calculated by [diffing][mapPatchFromFullDiff] the
+ * given [State]'s [transitions].
*/
fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> {
if (this is Incremental<K, V>) return this
@@ -264,34 +199,6 @@
)
}
-/** Returns an [Incremental] that acts like the current value of the given [State]. */
-fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
- val stateChangePatches =
- transitions.mapNotNull { (old, new) ->
- mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
- }
- val innerChanges =
- map { inner ->
- merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
- switchPatch + upcomingPatch
- }
- }
- .switchEventsPromptly()
- val flattened = flatten()
- return IncrementalInit(
- init("switchIncremental") {
- val upstream = flattened.init.connect(this)
- IncrementalImpl(
- "switchIncremental",
- "switchIncremental",
- upstream.changes,
- innerChanges.init.connect(this),
- upstream.store,
- )
- }
- )
-}
-
private inline fun <K, V> deferInline(
crossinline block: InitScope.() -> Incremental<K, V>
): Incremental<K, V> = IncrementalInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
index 77598b3..19e3fcd 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
@@ -23,16 +23,15 @@
import com.android.systemui.kairos.internal.util.childScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
-/**
- * Marks declarations that are still **experimental** and shouldn't be used in general production
- * code.
- */
+/** Marks APIs that are still **experimental** and shouldn't be used in general production code. */
@RequiresOptIn(
message = "This API is experimental and should not be used in general production code."
)
@@ -139,37 +138,46 @@
private val endSignal: Events<Any>,
) : KairosNetwork {
override suspend fun <R> transact(block: TransactionScope.() -> R): R =
- network.transaction("KairosNetwork.transact") { block() }.await()
+ network.transaction("KairosNetwork.transact") { block() }.awaitOrCancel()
override suspend fun activateSpec(spec: BuildSpec<*>) {
- val stopEmitter =
- CoalescingMutableEvents(
- name = "activateSpec",
- coalesce = { _, _: Unit -> },
- network = network,
- getInitialValue = {},
- )
- val job =
- network
- .transaction("KairosNetwork.activateSpec") {
- val buildScope =
- BuildScopeImpl(
- stateScope =
- StateScopeImpl(
- evalScope = this,
- endSignal = mergeLeft(stopEmitter, endSignal),
- ),
- coroutineScope = scope,
- )
- buildScope.launchScope(spec)
+ val stopEmitter = conflatedMutableEvents<Unit>()
+ network
+ .transaction("KairosNetwork.activateSpec") {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope =
+ StateScopeImpl(
+ evalScope = this,
+ endSignalLazy = lazy { mergeLeft(stopEmitter, endSignal) },
+ ),
+ coroutineScope = scope,
+ )
+ buildScope.launchScope {
+ spec.applySpec()
+ launchEffect { awaitCancellationAndThen { stopEmitter.emit(Unit) } }
}
- .await()
- awaitCancellationAndThen {
- stopEmitter.emit(Unit)
- job.cancel()
- }
+ }
+ .awaitOrCancel()
+ .joinOrCancel()
}
+ private suspend fun <T> Deferred<T>.awaitOrCancel(): T =
+ try {
+ await()
+ } catch (ex: CancellationException) {
+ cancel(ex)
+ throw ex
+ }
+
+ private suspend fun Job.joinOrCancel(): Unit =
+ try {
+ join()
+ } catch (ex: CancellationException) {
+ cancel(ex)
+ throw ex
+ }
+
override fun <In, Out> coalescingMutableEvents(
coalesce: (old: Out, new: In) -> Out,
getInitialValue: () -> Out,
@@ -214,3 +222,45 @@
scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() }
return RootKairosNetwork(network, scope, scope.coroutineContext.job)
}
+
+@ExperimentalKairosApi
+interface HasNetwork : KairosScope {
+ /**
+ * A [KairosNetwork] handle that is bound to the lifetime of a [BuildScope].
+ *
+ * It supports all of the standard functionality by which external code can interact with this
+ * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
+ * children to the [BuildScope], such that when the [BuildScope] is destroyed, all children are
+ * also destroyed.
+ */
+ val kairosNetwork: KairosNetwork
+}
+
+/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableEvents(): MutableEvents<T> = MutableEvents(kairosNetwork)
+
+/** Returns a [MutableState] with initial state [initialValue]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableState(initialValue: T): MutableState<T> =
+ MutableState(kairosNetwork, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableEvents<In, Out> = CoalescingMutableEvents(kairosNetwork, coalesce, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+): CoalescingMutableEvents<In, Out> =
+ CoalescingMutableEvents(kairosNetwork, coalesce, getInitialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.ConflatedMutableEvents(): CoalescingMutableEvents<T, T> =
+ ConflatedMutableEvents(kairosNetwork)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
index ce3e923..e526f45 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
@@ -16,42 +16,11 @@
package com.android.systemui.kairos
-import com.android.systemui.kairos.internal.CompletableLazy
-
/** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */
@DslMarker annotation class KairosScopeMarker
/**
- * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in
+ * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from inner
* lambdas.
*/
-@KairosScopeMarker
-@ExperimentalKairosApi
-interface KairosScope {
- /** Returns the value held by the [DeferredValue], suspending until available if necessary. */
- fun <A> DeferredValue<A>.get(): A = unwrapped.value
-}
-
-/**
- * A value that may not be immediately (synchronously) available, but is guaranteed to be available
- * before this transaction is completed.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi
-class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>)
-
-/**
- * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not
- * yet available.
- *
- * This API is not meant for general usage within the Kairos network. It is made available mainly
- * for debugging and logging. You should always prefer [get][KairosScope.get] if possible.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value
-
-/** Returns an already-available [DeferredValue] containing [value]. */
-@ExperimentalKairosApi
-fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
+@KairosScopeMarker @ExperimentalKairosApi interface KairosScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
new file mode 100644
index 0000000..de9dca4
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.awaitValues
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.mergeNodes
+import com.android.systemui.kairos.internal.mergeNodesLeft
+import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
+import com.android.systemui.kairos.internal.switchDeferredImpl
+import com.android.systemui.kairos.internal.switchPromptImpl
+import com.android.systemui.kairos.util.map
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from both.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.mergeWith(
+ * other: Events<A>,
+ * transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+ * ): Events<A> =
+ * listOf(this, other).merge().map { it.reduce(transformCoincidence) }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.mergeWith(
+ other: Events<A>,
+ transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+): Events<A> {
+ val node =
+ mergeNodes(
+ getPulse = { init.connect(evalScope = this) },
+ getOther = { other.init.connect(evalScope = this) },
+ ) { a, b ->
+ transformCoincidence(a, b)
+ }
+ return EventsInit(constInit(name = null, node))
+}
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ * fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * ``` kotlin
+ * fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ * merge(*events).map { l -> l.reduce(transformCoincidence) }
+ * ```
+ */
+fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ merge(*events).map { l -> l.reduce(transformCoincidence) }
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.merge
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
+ EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * Semantically equivalent to the following definition:
+ * ``` kotlin
+ * fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ * merge().mapCheap { it.first() }
+ * ```
+ *
+ * In reality, the implementation avoids allocating the intermediate list of all coincident
+ * emissions.
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ * fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ */
+@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [Map], and are given the same key of the associated [Events] in the
+ * input [Map].
+ *
+ * ``` kotlin
+ * fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ * asSequence()
+ * .map { (k, events) -> events.map { a -> k to a } }
+ * .toList()
+ * .merge()
+ * .map { it.toMap() }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ asSequence()
+ .map { (k, events) -> events.map { a -> k to a } }
+ .toList()
+ .merge()
+ .map { it.toMap() }
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
+ * map { it.merge() }.switchEvents()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementally
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
+ val operatorName = "mergeEventsIncrementally"
+ val name = operatorName
+ val patches =
+ mapImpl({ init.connect(this).patches }) { patch, _ ->
+ patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+ }
+ return EventsInit(
+ constInit(
+ name,
+ switchDeferredImpl(
+ name = name,
+ getStorage = {
+ init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValues { (_, events) -> events.init.connect(this) }
+ .asIterable()
+ },
+ getPatches = { patches },
+ storeFactory = ConcurrentHashMapK.Factory(),
+ )
+ .awaitValues(),
+ )
+ )
+}
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
+ * map { it.merge() }.switchEventsPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementallyPromptly
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
+ val operatorName = "mergeEventsIncrementallyPromptly"
+ val name = operatorName
+ val patches =
+ mapImpl({ init.connect(this).patches }) { patch, _ ->
+ patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+ }
+ return EventsInit(
+ constInit(
+ name,
+ switchPromptImpl(
+ name = name,
+ getStorage = {
+ init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValues { (_, events) -> events.init.connect(this) }
+ .asIterable()
+ },
+ getPatches = { patches },
+ storeFactory = ConcurrentHashMapK.Factory(),
+ )
+ .awaitValues(),
+ )
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
new file mode 100644
index 0000000..6c070a6
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
+ * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
+ * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
+ * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
+ *
+ * Use [compiledBuildSpec] to compile and stand-up a mode graph.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+fun interface BuildMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+ * new mode.
+ */
+ fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
+}
+
+/**
+ * Returns a [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
+ * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
+ get() = buildSpec {
+ var modeChangeEvents by EventsLoop<BuildMode<A>>()
+ val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { buildSpec { enableMode() } } }
+ .holdLatestSpec(buildSpec { enableMode() })
+ modeChangeEvents =
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
+ activeMode.map { it.first }
+ }
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all state accumulation is immediately started. When the returned
+ * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
+ * accumulation in the process.
+ *
+ * Use [compiledStateful] to compile and stand-up a mode graph.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+fun interface StatefulMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+ * new mode.
+ */
+ fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
+}
+
+/**
+ * Returns a [Stateful] that, when [applied][StateScope.applyStateful], stands up a modal-transition
+ * graph starting with this [StatefulMode], automatically switching to new modes as they are
+ * produced.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
+ get() = statefully {
+ var modeChangeEvents by EventsLoop<StatefulMode<A>>()
+ val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { statefully { enableMode() } } }
+ .holdLatestStateful(statefully { enableMode() })
+ modeChangeEvents =
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
+ activeMode.map { it.first }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
new file mode 100644
index 0000000..f7decbb
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.DerivedMapCheap
+import com.android.systemui.kairos.internal.StateImpl
+import com.android.systemui.kairos.internal.init
+
+/**
+ * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
+ * holding a specific value.
+ *
+ * An example:
+ * ```
+ * val intState: State<Int> = ...
+ * val intSelector: StateSelector<Int> = intState.selector()
+ * // Tracks if intState is holding 1
+ * val isOne: State<Boolean> = intSelector.whenSelected(1)
+ * ```
+ *
+ * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
+ * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [State.map] operations used to track a
+ * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
+ * [State] to update, and so operates in `O(1)`.
+ *
+ * Note that the returned [StateSelector] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
+ StateSelector(
+ this,
+ changes
+ .map { new -> mapOf(new to true, sampleDeferred().value to false) }
+ .groupByKey(numDistinctValues),
+ )
+
+/**
+ * Tracks the currently selected value of type [A] from an upstream [State].
+ *
+ * @see selector
+ */
+@ExperimentalKairosApi
+class StateSelector<in A>
+internal constructor(
+ private val upstream: State<A>,
+ private val groupedChanges: GroupedEvents<A, Boolean>,
+) {
+ /**
+ * Returns a [State] that tracks whether the upstream [State] is currently holding the given
+ * [value].
+ *
+ * @see selector
+ */
+ fun whenSelected(value: A): State<Boolean> {
+ val operatorName = "StateSelector#whenSelected"
+ val name = "$operatorName[$value]"
+ return StateInit(
+ init(name) {
+ StateImpl(
+ name,
+ operatorName,
+ groupedChanges.impl.eventsForKey(value),
+ DerivedMapCheap(upstream.init) { it == value },
+ )
+ }
+ )
+ }
+
+ operator fun get(value: A): State<Boolean> = whenSelected(value)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
index 1f0a19d..22ca83c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
@@ -17,7 +17,6 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DerivedMapCheap
import com.android.systemui.kairos.internal.EventsImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
@@ -37,20 +36,31 @@
import com.android.systemui.kairos.internal.mapStateImpl
import com.android.systemui.kairos.internal.mapStateImplCheap
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.zipStateMap
-import com.android.systemui.kairos.internal.zipStates
+import com.android.systemui.kairos.util.WithPrev
import kotlin.reflect.KProperty
/**
- * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
- * holds a value, and an [Events] that emits when the value changes.
+ * A time-varying value with discrete changes. Conceptually, a combination of a [Transactional] that
+ * holds a value, and an [Events] that emits when the value [changes].
+ *
+ * [States][State] follow these rules:
+ * 1. In the same transaction that [changes] emits a new value, [sample] will continue to return the
+ * previous value.
+ * 2. Unless it is [constant][stateOf], [States][State] can only be created via [StateScope]
+ * operations, or derived from other existing [States][State] via [State.map], [combine], etc.
+ * 3. [States][State] can only be [sampled][TransactionScope.sample] within a [TransactionScope].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.states
*/
@ExperimentalKairosApi
sealed class State<out A> {
internal abstract val init: Init<StateImpl<A>>
}
-/** A [State] that never changes. */
+/**
+ * Returns a constant [State] that never changes. [changes] is equivalent to [emptyEvents] and
+ * [TransactionScope.sample] will always produce [value].
+ */
@ExperimentalKairosApi
fun <A> stateOf(value: A): State<A> {
val operatorName = "stateOf"
@@ -65,6 +75,10 @@
* will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<State<A>>.defer() = deferredState { value }
+ * ```
*/
@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value }
@@ -76,6 +90,10 @@
* and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<State<A>>.defer() = deferredState { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value }
@@ -94,6 +112,8 @@
/**
* Returns a [State] containing the results of applying [transform] to the value held by the
* original [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapState
*/
@ExperimentalKairosApi
fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
@@ -110,10 +130,12 @@
* Returns a [State] that transforms the value held inside this [State] by applying it to the
* [transform].
*
- * Note that unlike [map], the result is not cached. This means that not only should [transform] be
- * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for
- * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an
- * observable change to the returned [State].
+ * Note that unlike [State.map], the result is not cached. This means that not only should
+ * [transform] be fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that
+ * [changes] for the returned [State] will operate unexpectedly, emitting at rates that do not
+ * reflect an observable change to the returned [State].
+ *
+ * @see State.map
*/
@ExperimentalKairosApi
fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
@@ -125,214 +147,30 @@
}
/**
- * Returns a [State] by combining the values held inside the given [State]s by applying them to the
- * given function [transform].
- */
-@ExperimentalKairosApi
-fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
- combine(this, other, transform)
-
-/**
* Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half
* of the original.
*
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ * fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
+ * val first = map { it.first }
+ * val second = map { it.second }
+ * return first to second
+ * }
* ```
*/
@ExperimentalKairosApi
fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
- val left = map { it.first }
- val right = map { it.second }
- return left to right
+ val first = map { it.first }
+ val second = map { it.second }
+ return first to second
}
/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [List].
+ * Returns a [State] by applying [transform] to the value held by the original [State].
*
- * @see State.combineWith
+ * @sample com.android.systemui.kairos.KairosSamples.flatMap
*/
@ExperimentalKairosApi
-fun <A> Iterable<State<A>>.combine(): State<List<A>> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- val states = map { it.init }
- zipStates(
- name,
- operatorName,
- states.size,
- states = init(null) { states.map { it.connect(this) } },
- )
- }
- )
-}
-
-/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStateMap(
- name,
- operatorName,
- size,
- states = init(null) { mapValues { it.value.init.connect(evalScope = this) } },
- )
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
- combine().map(transform)
-
-/**
- * Returns a [State] by combining the values held inside the given [State]s into a [List].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
- states.asIterable().combine(transform)
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- transform: KairosScope.(A, B) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
- NoScope.transform(a, b)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- transform: KairosScope.(A, B, C) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
- NoScope.transform(a, b, c)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, D, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- stateD: State<D>,
- transform: KairosScope.(A, B, C, D) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
- a,
- b,
- c,
- d ->
- NoScope.transform(a, b, c, d)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, D, E, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- stateD: State<D>,
- stateE: State<E>,
- transform: KairosScope.(A, B, C, D, E) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(
- name,
- operatorName,
- stateA.init,
- stateB.init,
- stateC.init,
- stateD.init,
- stateE.init,
- ) { a, b, c, d, e ->
- NoScope.transform(a, b, c, d, e)
- }
- }
- )
-}
-
-/** Returns a [State] by applying [transform] to the value held by the original [State]. */
-@ExperimentalKairosApi
fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> {
val operatorName = "flatMap"
val name = operatorName
@@ -345,77 +183,18 @@
)
}
-/** Shorthand for `flatMap { it }` */
+/**
+ * Returns a [State] that behaves like the current value of the original [State].
+ *
+ * ``` kotlin
+ * fun <A> State<State<A>>.flatten() = flatMap { it }
+ * ```
+ *
+ * @see flatMap
+ */
@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it }
/**
- * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
- * holding a specific value.
- *
- * An example:
- * ```
- * val intState: State<Int> = ...
- * val intSelector: StateSelector<Int> = intState.selector()
- * // Tracks if lInt is holding 1
- * val isOne: State<Boolean> = intSelector.whenSelected(1)
- * ```
- *
- * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
- * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [State.map] operations used to track a
- * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
- * [State] to update, and so operates in `O(1)`.
- *
- * Note that the returned [StateSelector] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
- StateSelector(
- this,
- changes
- .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
- .groupByKey(numDistinctValues),
- )
-
-/**
- * Tracks the currently selected value of type [A] from an upstream [State].
- *
- * @see selector
- */
-@ExperimentalKairosApi
-class StateSelector<in A>
-internal constructor(
- private val upstream: State<A>,
- private val groupedChanges: GroupedEvents<A, Boolean>,
-) {
- /**
- * Returns a [State] that tracks whether the upstream [State] is currently holding the given
- * [value].
- *
- * @see selector
- */
- fun whenSelected(value: A): State<Boolean> {
- val operatorName = "StateSelector#whenSelected"
- val name = "$operatorName[$value]"
- return StateInit(
- init(name) {
- StateImpl(
- name,
- operatorName,
- groupedChanges.impl.eventsForKey(value),
- DerivedMapCheap(upstream.init) { it == value },
- )
- }
- )
- }
-
- operator fun get(value: A): State<Boolean> = whenSelected(value)
-}
-
-/**
* A mutable [State] that provides the ability to manually [set its value][setValue].
*
* Multiple invocations of [setValue] that occur before a transaction are conflated; only the most
@@ -441,6 +220,9 @@
override val init: Init<StateImpl<T>>
get() = state.init
+ // TODO: not convinced this is totally safe
+ // - at least for the BuildScope smart-constructor, we can avoid the network.transaction { }
+ // call since we're already in a transaction
internal val state = run {
val changes = input.impl
val name = null
@@ -491,7 +273,17 @@
fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped)
}
-/** A forward-reference to a [State], allowing for recursive definitions. */
+/**
+ * A forward-reference to a [State]. Useful for recursive definitions.
+ *
+ * This reference can be used like a standard [State], but will throw an error if its [loopback] is
+ * unset before it is [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ *
+ * Note that it is safe to invoke [TransactionScope.sampleDeferred] before [loopback] is set,
+ * provided the returned [DeferredValue] is not [queried][KairosScope.get].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.stateLoop
+ */
@ExperimentalKairosApi
class StateLoop<A> : State<A>() {
@@ -502,7 +294,10 @@
override val init: Init<StateImpl<A>> =
init(name) { deferred.value.init.connect(evalScope = this) }
- /** The [State] this [StateLoop] will forward to. */
+ /**
+ * The [State] this reference is referring to. Must be set before this [StateLoop] is
+ * [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ */
var loopback: State<A>? = null
set(value) {
value?.let {
@@ -528,3 +323,24 @@
private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> =
StateInit(init(name = null) { block().init.connect(evalScope = this) })
+
+/**
+ * Like [changes] but also includes the old value of this [State].
+ *
+ * Shorthand for:
+ * ``` kotlin
+ * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
+ * ```
+ */
+@ExperimentalKairosApi
+val <A> State<A>.transitions: Events<WithPrev<A, A>>
+ get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
+
+/**
+ * Returns an [Events] that emits the new value of this [State] when it changes.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.changes
+ */
+@ExperimentalKairosApi
+val <A> State<A>.changes: Events<A>
+ get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
index 933ff1a..faeffe8 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
@@ -16,13 +16,11 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.util.MapPatch
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.mapMaybeValues
-import com.android.systemui.kairos.util.none
import com.android.systemui.kairos.util.zipWith
// TODO: caching story? should each Scope have a cache of applied Stateful instances?
@@ -64,6 +62,8 @@
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * @see holdState
*/
fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A>
@@ -71,113 +71,128 @@
* Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
*
* The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
- * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
- * an associated value of [none] will remove the key from the tracked [Map].
+ * map, an associated value of [present][Maybe.present] will insert or replace the value in the
+ * tracked [Map], and an associated value of [absent][Maybe.absent] will remove the key from the
+ * tracked [Map].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ * @see MapPatch
*/
- fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
initialValues: DeferredValue<Map<K, V>>
): Incremental<K, V>
+ /**
+ * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
+ * original [Events].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
+ * of the original [Events].
+ */
+ fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
+
+ /**
+ * Returns an [Events] containing the results of applying each [Stateful] emitted from the
+ * original [Events], and a [DeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
+ * with the same key is stopped.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ */
+ fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
+ init: DeferredValue<Map<K, Stateful<B>>>,
+ numKeys: Int? = null,
+ ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>>
+
// TODO: everything below this comment can be made into extensions once we have context params
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- * initialEvents: Map<K, Events<V>>,
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ * initialEvents: DeferredValue<Map<K, Events<V>>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementally
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- name: String? = null,
- initialEvents: DeferredValue<Map<K, Events<V>>>,
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ initialEvents: DeferredValue<Map<K, Events<V>>>
): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementally()
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- * initialEvents: Map<K, Events<V>>,
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ * initialEvents: DeferredValue<Map<K, Events<V>>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementallyPromptly
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- initialEvents: DeferredValue<Map<K, Events<V>>>,
- name: String? = null,
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ initialEvents: DeferredValue<Map<K, Events<V>>>
): Events<Map<K, V>> =
foldStateMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly()
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
* initialEvents: Map<K, Events<V>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementally
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- name: String? = null,
- initialEvents: Map<K, Events<V>> = emptyMap(),
- ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents))
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ initialEvents: Map<K, Events<V>> = emptyMap()
+ ): Events<Map<K, V>> = mergeEventsIncrementally(deferredOf(initialEvents))
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
* initialEvents: Map<K, Events<V>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementallyPromptly
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- initialEvents: Map<K, Events<V>> = emptyMap(),
- name: String? = null,
- ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name)
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ initialEvents: Map<K, Events<V>> = emptyMap()
+ ): Events<Map<K, V>> = mergeEventsIncrementallyPromptly(deferredOf(initialEvents))
/** Applies the [Stateful] within this [StateScope]. */
fun <A> Stateful<A>.applyStateful(): A = this()
/**
- * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue].
+ * Applies the [Stateful] within this [StateScope], returning the result as a [DeferredValue].
*/
fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope {
applyStateful()
@@ -190,42 +205,59 @@
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.holdState
+ * @see holdStateDeferred
*/
fun <A> Events<A>.holdState(initialValue: A): State<A> =
holdStateDeferred(deferredOf(initialValue))
/**
- * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
- * original [Events].
- *
- * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
- * of the original [Events].
- */
- fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
-
- /**
* Returns an [Events] containing the results of applying [transform] to each value of the
* original [Events].
*
* [transform] can perform state accumulation via its [StateScope] receiver. Unlike
* [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
* original [Events].
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
+ * map { statefully { transform(it) } }.applyStatefuls()
+ * ```
*/
fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
- map { statefully { transform(it) } }.applyStatefuls()
+ mapCheap { statefully { transform(it) } }.applyStatefuls()
/**
* Returns a [State] the holds the result of applying the [Stateful] held by the original
* [State].
*
* Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
+ *
+ * ``` kotlin
+ * fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
+ * changes
+ * .applyStatefuls()
+ * .holdState(initialValue = sample().applyStateful())
+ * ```
*/
fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
changes
.applyStatefuls()
- .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+ .holdStateDeferred(
+ initialValue = deferredStateScope { sampleDeferred().value.applyStateful() }
+ )
- /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */
+ /**
+ * Returns an [Events] that acts like the most recent [Events] to be emitted from the original
+ * [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
+ * ```
+ *
+ * @see switchEvents
+ */
fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
/**
@@ -234,9 +266,14 @@
*
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
+ * map { statefully { transform(it) } }.applyLatestStateful()
+ * ```
*/
fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
- map { statefully { transform(it) } }.applyLatestStateful()
+ mapCheap { statefully { transform(it) } }.applyLatestStateful()
/**
* Returns an [Events] that switches to a new [Events] produced by [transform] every time the
@@ -244,6 +281,13 @@
*
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.flatMapLatestStateful(
+ * transform: StateScope.(A) -> Events<B>
+ * ): Events<B> =
+ * mapLatestStateful(transform).flatten()
+ * ```
*/
fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> =
mapLatestStateful(transform).flatten()
@@ -254,6 +298,8 @@
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
* stopped.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.applyLatestStateful
*/
fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first
@@ -281,19 +327,18 @@
init: Stateful<A>
): Pair<Events<B>, DeferredValue<A>> {
val (events, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
+ mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
.applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
val outEvents: Events<B> =
events.mapMaybe {
checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
}
val outInit: DeferredValue<A> = deferredTransactionScope {
- val initResult: Map<Unit, A> = result.get()
+ val initResult: Map<Unit, A> = result.value
check(Unit in initResult) {
"applyLatest: expected initial result, but none present in: $initResult"
}
- @Suppress("UNCHECKED_CAST")
- initResult.getOrDefault(Unit) { null } as A
+ initResult.getValue(Unit)
}
return Pair(outEvents, outInit)
}
@@ -303,34 +348,32 @@
* original [Events], and a [DeferredValue] containing the result of applying [init]
* immediately.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
- *
- * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
- * with the same key is stopped.
- */
- fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
- init: DeferredValue<Map<K, Stateful<B>>>,
- numKeys: Int? = null,
- ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
-
- /**
- * Returns an [Events] containing the results of applying each [Stateful] emitted from the
- * original [Events], and a [DeferredValue] containing the result of applying [init]
- * immediately.
- *
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
init: Map<K, Stateful<B>>,
numKeys: Int? = null,
- ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>> =
applyLatestStatefulForKey(deferredOf(init), numKeys)
+ /**
+ * Returns an [Incremental] containing the latest results of applying each [Stateful] emitted
+ * from the original [Incremental]'s [updates].
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
+ * with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ */
fun <K, V> Incremental<K, Stateful<V>>.applyLatestStatefulForKey(
numKeys: Int? = null
): Incremental<K, V> {
@@ -345,10 +388,12 @@
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
init: DeferredValue<Map<K, Stateful<A>>>,
numKeys: Int? = null,
): Incremental<K, A> {
@@ -363,28 +408,33 @@
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
init: Map<K, Stateful<A>> = emptyMap(),
numKeys: Int? = null,
): Incremental<K, A> = holdLatestStatefulForKey(deferredOf(init), numKeys)
/**
* Returns an [Events] containing the results of applying each [Stateful] emitted from the
- * original [Events], and a [DeferredValue] containing the result of applying [stateInit]
- * immediately.
+ * original [Events].
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.applyLatestStatefulForKey
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
numKeys: Int? = null
- ): Events<Map<K, Maybe<A>>> =
+ ): Events<MapPatch<K, A>> =
applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first
/**
@@ -395,18 +445,20 @@
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
initialValues: DeferredValue<Map<K, A>>,
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
.applyLatestStatefulForKey(
deferredStateScope {
- initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+ initialValues.value.mapValues { (_, v) -> statefully { transform(v) } }
},
numKeys = numKeys,
)
@@ -419,14 +471,16 @@
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
initialValues: Map<K, A>,
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
/**
@@ -436,13 +490,24 @@
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ *
+ * ``` kotlin
+ * fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
+ * numKeys: Int? = null,
+ * transform: StateScope.(A) -> B,
+ * ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
+ * map { patch -> patch.mapValues { (_, mv) -> mv.map { statefully { transform(it) } } } }
+ * .applyLatestStatefulForKey(numKeys)
+ * ```
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+ ): Events<MapPatch<K, B>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
/**
* Returns an [Events] that will only emit the next event of the original [Events], and then
@@ -450,18 +515,31 @@
*
* If the original [Events] is emitting an event at this exact time, then it will be the only
* even emitted from the result [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.nextOnly(): Events<A> =
+ * EventsLoop<A>().apply {
+ * loopback = map { emptyEvents }.holdState(this@nextOnly).switchEvents()
+ * }
+ * ```
*/
- fun <A> Events<A>.nextOnly(name: String? = null): Events<A> =
+ fun <A> Events<A>.nextOnly(): Events<A> =
if (this === emptyEvents) {
this
} else {
- EventsLoop<A>().also {
- it.loopback =
- it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name)
+ EventsLoop<A>().apply {
+ loopback = mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents()
}
}
- /** Returns an [Events] that skips the next emission of the original [Events]. */
+ /**
+ * Returns an [Events] that skips the next emission of the original [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.skipNext(): Events<A> =
+ * nextOnly().map { this@skipNext }.holdState(emptyEvents).switchEvents()
+ * ```
+ */
fun <A> Events<A>.skipNext(): Events<A> =
if (this === emptyEvents) {
this
@@ -475,6 +553,11 @@
*
* If the original [Events] emits at the same time as [stop], then the returned [Events] will
* emit that value.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
+ * stop.map { emptyEvents }.nextOnly().holdState(this).switchEvents()
+ * ```
*/
fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
if (stop === emptyEvents) {
@@ -494,14 +577,19 @@
val (_, init: DeferredValue<Map<Unit, A>>) =
stop
.nextOnly()
- .map { mapOf(Unit to none<Stateful<A>>()) }
+ .map { mapOf(Unit to Maybe.absent<Stateful<A>>()) }
.applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
- return deferredStateScope { init.get().getValue(Unit) }
+ return deferredStateScope { init.value.getValue(Unit) }
}
/**
* Returns an [Events] that emits values from the original [Events] up to and including a value
* is emitted that satisfies [predicate].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ * takeUntil(filter(predicate))
+ * ```
*/
fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
takeUntil(filter(predicate))
@@ -513,6 +601,18 @@
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.foldState(
+ * initialValue: B,
+ * transform: TransactionScope.(A, B) -> B,
+ * ): State<B> {
+ * lateinit var state: State<B>
+ * return map { a -> transform(a, state.sample()) }
+ * .holdState(initialValue)
+ * .also { state = it }
+ * }
+ * ```
*/
fun <A, B> Events<A>.foldState(
initialValue: B,
@@ -529,6 +629,18 @@
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.foldStateDeferred(
+ * initialValue: DeferredValue<B>,
+ * transform: TransactionScope.(A, B) -> B,
+ * ): State<B> {
+ * lateinit var state: State<B>
+ * return map { a -> transform(a, state.sample()) }
+ * .holdStateDeferred(initialValue)
+ * .also { state = it }
+ * }
+ * ```
*/
fun <A, B> Events<A>.foldStateDeferred(
initialValue: DeferredValue<B>,
@@ -551,10 +663,11 @@
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
*
- * Shorthand for:
- * ```kotlin
- * val (changes, initApplied) = applyLatestStateful(init)
- * return changes.holdStateDeferred(initApplied)
+ * ``` kotlin
+ * fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
+ * val (changes, initApplied) = applyLatestStateful(init)
+ * return changes.holdStateDeferred(initApplied)
+ * }
* ```
*/
fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
@@ -578,8 +691,8 @@
* that the returned [Events] will not emit until the original [Events] has emitted twice.
*/
fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> =
- mapCheap { just(it) }
- .pairwise(none)
+ mapCheap { Maybe.present(it) }
+ .pairwise(Maybe.absent)
.mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
/**
@@ -599,10 +712,11 @@
* Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
*
* The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
- * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
- * an associated value of [none] will remove the key from the tracked [Map].
+ * map, an associated value of [Maybe.present] will insert or replace the value in the tracked
+ * [Map], and an associated value of [absent][Maybe.absent] will remove the key from the tracked
+ * [Map].
*/
- fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
initialValues: Map<K, V> = emptyMap()
): Incremental<K, V> = foldStateMapIncrementally(deferredOf(initialValues))
@@ -610,10 +724,11 @@
* Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue],
* containing the emitted value and its index (starting from zero).
*
- * Shorthand for:
- * ```
- * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
- * sample(index) { a, idx -> IndexedValue(idx, a) }
+ * ``` kotlin
+ * fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
+ * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
+ * return sample(index) { a, idx -> IndexedValue(idx, a) }
+ * }
* ```
*/
fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
@@ -625,9 +740,11 @@
* Returns an [Events] containing the results of applying [transform] to each value of the
* original [Events] and its index (starting from zero).
*
- * Shorthand for:
- * ```
- * withIndex().map { (idx, a) -> transform(idx, a) }
+ * ``` kotlin
+ * fun <A> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
+ * val index = foldState(0) { _, i -> i + 1 }
+ * return sample(index) { a, idx -> transform(idx, a) }
+ * }
* ```
*/
fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
@@ -635,7 +752,16 @@
return sample(index) { a, idx -> transform(idx, a) }
}
- /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */
+ /**
+ * Returns an [Events] where all subsequent repetitions of the same value are filtered out.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.distinctUntilChanged(): Events<A> {
+ * val state: State<Any?> = holdState(Any())
+ * return filter { it != state.sample() }
+ * }
+ * ```
+ */
fun <A> Events<A>.distinctUntilChanged(): Events<A> {
val state: State<Any?> = holdState(Any())
return filter { it != state.sample() }
@@ -647,18 +773,35 @@
*
* Note that the returned [Events] will not emit anything until [other] has emitted at least one
* value.
+ *
+ * ``` kotlin
+ * fun <A, B, C> Events<A>.sample(
+ * other: Events<B>,
+ * transform: TransactionScope.(A, B) -> C,
+ * ): Events<C> {
+ * val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+ * return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
+ * }
+ * ```
*/
fun <A, B, C> Events<A>.sample(
other: Events<B>,
transform: TransactionScope.(A, B) -> C,
): Events<C> {
- val state = other.mapCheap { just(it) }.holdState(none)
- return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
+ val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+ return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
}
/**
* Returns a [State] that samples the [Transactional] held by the given [State] within the same
* transaction that the state changes.
+ *
+ * ``` kotlin
+ * fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
+ * changes
+ * .sampleTransactionals()
+ * .holdStateDeferred(deferredTransactionScope { sample().sample() })
+ * ```
*/
fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
changes
@@ -668,6 +811,14 @@
/**
* Returns a [State] that transforms the value held inside this [State] by applying it to the
* given function [transform].
+ *
+ * Note that this is less efficient than [State.map], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
+ * map { transactionally { transform(it) } }.sampleTransactionals()
+ * ```
*/
fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
map { transactionally { transform(it) } }.sampleTransactionals()
@@ -676,7 +827,20 @@
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B, Z> combineTransactionally(
+ * stateA: State<A>,
+ * stateB: State<B>,
+ * transform: TransactionScope.(A, B) -> Z,
+ * ): State<Z> =
+ * combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
+ * .sampleTransactionals()
+ * ```
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, Z> combineTransactionally(
stateA: State<A>,
@@ -690,7 +854,10 @@
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, C, Z> combineTransactionally(
stateA: State<A>,
@@ -705,7 +872,10 @@
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, C, D, Z> combineTransactionally(
stateA: State<A>,
@@ -719,7 +889,18 @@
}
.sampleTransactionals()
- /** Returns a [State] by applying [transform] to the value held by the original [State]. */
+ /**
+ * Returns a [State] by applying [transform] to the value held by the original [State].
+ *
+ * Note that this is less efficient than [flatMap], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B> State<A>.flatMapTransactionally(
+ * transform: TransactionScope.(A) -> State<B>
+ * ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
+ * ```
+ */
fun <A, B> State<A>.flatMapTransactionally(
transform: TransactionScope.(A) -> State<B>
): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
@@ -728,7 +909,10 @@
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, Z> combineTransactionally(
vararg states: State<A>,
@@ -739,7 +923,10 @@
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, Z> Iterable<State<A>>.combineTransactionally(
transform: TransactionScope.(List<A>) -> Z
@@ -748,8 +935,13 @@
/**
* Returns a [State] by combining the values held inside the given [State]s by applying them to
* the given function [transform].
+ *
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
*/
- fun <A, B, C> State<A>.combineWithTransactionally(
+ @Suppress("INAPPLICABLE_JVM_NAME")
+ @JvmName(name = "combineStateTransactionally")
+ fun <A, B, C> State<A>.combineTransactionally(
other: State<B>,
transform: TransactionScope.(A, B) -> C,
): State<C> = combineTransactionally(this, other, transform)
@@ -757,6 +949,15 @@
/**
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the state of the original's values.
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
+ * mapValues { (_, v) -> v.changes }
+ * .mergeEventsIncrementallyPromptly()
+ * .foldStateMapIncrementally(
+ * deferredStateScope { sample().mapMaybeValues { (_, s) -> s.sample() } }
+ * )
+ * ```
*/
fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
mapValues { (_, v) -> v.changes }
@@ -769,6 +970,12 @@
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the [State] returned from applying [transform] to the original's
* entries.
+ *
+ * ``` kotlin
+ * fun <K, V, U> Incremental<K, V>.mapIncrementalState(
+ * transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
+ * ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally()
+ * ```
*/
fun <K, V, U> Incremental<K, V>.mapIncrementalState(
transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
@@ -778,16 +985,33 @@
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the [State] returned from applying [transform] to the original's
* entries, such that entries are added when that state is `true`, and removed when `false`.
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, V>.filterIncrementally(
+ * transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
+ * ): Incremental<K, V> = mapIncrementalState { entry ->
+ * transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
+ * }
+ * ```
*/
fun <K, V> Incremental<K, V>.filterIncrementally(
transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
): Incremental<K, V> = mapIncrementalState { entry ->
- transform(entry).map { if (it) just(entry.value) else none }
+ transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
}
/**
* Returns an [Incremental] that samples the [Transactionals][Transactional] held by the
* original within the same transaction that the incremental [updates].
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
+ * updates
+ * .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } }
+ * .foldStateMapIncrementally(
+ * deferredStateScope { sample().mapValues { (k, v) -> v.sample() } }
+ * )
+ * ```
*/
fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
updates
@@ -799,6 +1023,16 @@
/**
* Returns an [Incremental] that tracks the entries of the original incremental, but values
* replaced with those obtained by applying [transform] to each original entry.
+ *
+ * Note that this is less efficient than [mapValues], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
+ * transform: TransactionScope.(Map.Entry<K, V>) -> U
+ * ): Incremental<K, U> =
+ * mapValues { transactionally { transform(it) } }.sampleTransactionals()
+ * ```
*/
fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
transform: TransactionScope.(Map.Entry<K, V>) -> U
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
new file mode 100644
index 0000000..63e27d0
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.internal.IncrementalImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.switchDeferredImplSingle
+import com.android.systemui.kairos.internal.switchPromptImplSingle
+import com.android.systemui.kairos.util.mapPatchFromFullDiff
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch does take effect until the *next* transaction after [State] changes. For a switch
+ * that takes effect immediately, see [switchEventsPromptly].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEvents
+ */
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEvents(): Events<A> {
+ val patches =
+ mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+ return EventsInit(
+ constInit(
+ name = null,
+ switchDeferredImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = { patches },
+ ),
+ )
+ )
+}
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch takes effect immediately within the same transaction that [State] changes. If the
+ * newly-switched-in [Events] is emitting a value within this transaction, then that value will be
+ * emitted from this switch. If not, but the previously-switched-in [Events] *is* emitting, then
+ * that value will be emitted from this switch instead. Otherwise, there will be no emission.
+ *
+ * In general, you should prefer [switchEvents] over this method. It is both safer and more
+ * performant.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEventsPromptly
+ */
+// TODO: parameter to handle coincidental emission from both old and new
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
+ val patches =
+ mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+ return EventsInit(
+ constInit(
+ name = null,
+ switchPromptImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = { patches },
+ ),
+ )
+ )
+}
+
+/** Returns an [Incremental] that behaves like current value of this [State]. */
+fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
+ val stateChangePatches =
+ transitions.mapNotNull { (old, new) ->
+ mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
+ }
+ val innerChanges =
+ map { inner ->
+ merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
+ switchPatch + upcomingPatch
+ }
+ }
+ .switchEventsPromptly()
+ val flattened = flatten()
+ return IncrementalInit(
+ init("switchIncremental") {
+ val upstream = flattened.init.connect(this)
+ IncrementalImpl(
+ "switchIncremental",
+ "switchIncremental",
+ upstream.changes,
+ innerChanges.init.connect(this),
+ upstream.store,
+ )
+ }
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
new file mode 100644
index 0000000..3d2768b
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
+ * transactionally connect to / disconnect from the [Events] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
+ * transactionally connect to / disconnect from the [State] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("eventsSpecToColdConflatedFlow")
+fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("stateSpecToColdConflatedFlow")
+fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [Events].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalFlowToColdConflatedFlow")
+fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalStateToColdConflatedFlow")
+fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulFlowToColdConflatedFlow")
+fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulStateToColdConflatedFlow")
+fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
index 2254169..a5ac909 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
@@ -16,11 +16,14 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.These
+
/**
* Kairos operations that are available while a transaction is active.
*
* These operations do not accumulate state, which makes [TransactionScope] weaker than
- * [StateScope], but allows them to be used in more places.
+ * [StateScope], but allows it to be used in more places.
*/
@ExperimentalKairosApi
interface TransactionScope : KairosScope {
@@ -57,7 +60,7 @@
*/
fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A>
- /** An [Events] that emits once, within this transaction, and then never again. */
+ /** An [Events] that emits once, within the current transaction, and then never again. */
val now: Events<Unit>
/**
@@ -66,7 +69,7 @@
*
* @see sampleDeferred
*/
- fun <A> State<A>.sample(): A = sampleDeferred().get()
+ fun <A> State<A>.sample(): A = sampleDeferred().value
/**
* Returns the current value held by this [Transactional]. Guaranteed to be consistent within
@@ -74,5 +77,55 @@
*
* @see sampleDeferred
*/
- fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+ fun <A> Transactional<A>.sample(): A = sampleDeferred().value
}
+
+/**
+ * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
+ * emission of the original [Events], within the same transaction of the original emission.
+ */
+@ExperimentalKairosApi
+fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, state.sample()) }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+ sampleable: Transactional<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, sampleable.sample()) }
+
+/**
+ * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
+ * the new value is passed to [transform].
+ *
+ * Note that [sample] is both more performant and safer to use with recursive definitions. You will
+ * generally want to use it rather than this.
+ *
+ * @see sample
+ */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.samplePromptly(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> =
+ sample(state) { a, b -> These.first(a to b) }
+ .mergeWith(state.changes.map { These.second(it) }) { thiz, that ->
+ These.both((thiz as These.First).value, (that as These.Second).value)
+ }
+ .mapMaybe { these ->
+ when (these) {
+ // both present, transform the upstream value and the new value
+ is These.Both -> Maybe.present(transform(these.first.first, these.second))
+ // no upstream present, so don't perform the sample
+ is These.Second -> Maybe.absent()
+ // just the upstream, so transform the upstream and the old value
+ is These.First -> Maybe.present(transform(these.value.first, these.value.second))
+ }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
index 9485cd21..cf98821 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
@@ -29,8 +29,8 @@
* it is "sampled", a new result may be produced.
*
* Because Kairos operates over an "idealized" model of Time that can be passed around as a data
- * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the
- * same (conceptual) time, in order to preserve _referential transparency_.
+ * type, [Transactionals][Transactional] are guaranteed to produce the same result if queried
+ * multiple times at the same (conceptual) time, in order to preserve _referential transparency_.
*/
@ExperimentalKairosApi
class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) {
@@ -50,6 +50,10 @@
* queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Transactional<A>>.defer() = deferredTransactional { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value }
@@ -62,6 +66,10 @@
* [value][Lazy.value] will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Transactional<A>>.defer() = deferredTransactional { value }
+ * ```
*/
@ExperimentalKairosApi
fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
@@ -89,7 +97,13 @@
/**
* Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per
* transaction; any subsequent sampling within the same transaction will receive a cached value.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.sampleTransactional
*/
@ExperimentalKairosApi
fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> =
Transactional(stateOf(transactionalImpl { block() }))
+
+/** Returns a [Transactional] that, when queried, samples this [State]. */
+fun <A> State<A>.asTransactional(): Transactional<A> =
+ Transactional(map { TransactionalImpl.Const(CompletableLazy(it)) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
index b20e77a..2f4c396 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
@@ -26,6 +26,7 @@
import com.android.systemui.kairos.Events
import com.android.systemui.kairos.EventsInit
import com.android.systemui.kairos.GroupedEvents
+import com.android.systemui.kairos.KairosCoroutineScope
import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.kairos.LocalNetwork
import com.android.systemui.kairos.MutableEvents
@@ -33,20 +34,23 @@
import com.android.systemui.kairos.groupByKey
import com.android.systemui.kairos.init
import com.android.systemui.kairos.internal.util.childScope
+import com.android.systemui.kairos.internal.util.invokeOnCancel
import com.android.systemui.kairos.internal.util.launchImmediate
import com.android.systemui.kairos.launchEffect
import com.android.systemui.kairos.mergeLeft
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import com.android.systemui.kairos.util.map
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.job
@@ -60,12 +64,8 @@
LocalNetwork(network, coroutineScope, endSignal)
}
- override fun <T> events(
- name: String?,
- builder: suspend EventProducerScope<T>.() -> Unit,
- ): Events<T> =
+ override fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T> =
buildEvents(
- name,
constructEvents = { inputNode ->
val events = MutableEvents(network, inputNode)
events to
@@ -123,9 +123,9 @@
val childScope = coroutineScope.childScope()
lateinit var cancelHandle: DisposableHandle
val handle = DisposableHandle {
- subRef.getAndSet(None)?.let { output ->
- cancelHandle.dispose()
- if (output is Just) {
+ cancelHandle.dispose()
+ subRef.getAndSet(Absent)?.let { output ->
+ if (output is Present) {
@Suppress("DeferredResultUnused")
network.transaction("observeEffect cancelled") {
scheduleDeactivation(output.value)
@@ -139,14 +139,27 @@
val outputNode =
Output<A>(
context = coroutineContext,
- onDeath = { subRef.set(None) },
+ onDeath = { subRef.set(Absent) },
onEmit = { output ->
- if (subRef.get() is Just) {
+ if (subRef.get() is Present) {
// Not cancelled, safe to emit
val scope =
object : EffectScope, TransactionScope by this {
- override val effectCoroutineScope: CoroutineScope = childScope
- override val kairosNetwork: KairosNetwork = localNetwork
+ override fun <R> async(
+ context: CoroutineContext,
+ start: CoroutineStart,
+ block: suspend KairosCoroutineScope.() -> R,
+ ): Deferred<R> =
+ childScope.async(context, start) {
+ object : KairosCoroutineScope, CoroutineScope by this {
+ override val kairosNetwork: KairosNetwork
+ get() = localNetwork
+ }
+ .block()
+ }
+
+ override val kairosNetwork: KairosNetwork
+ get() = localNetwork
}
scope.block(output)
}
@@ -162,7 +175,7 @@
.activate(evalScope = stateScope.evalScope, outputNode.schedulable)
?.let { (conn, needsEval) ->
outputNode.upstream = conn
- if (!subRef.compareAndSet(null, just(outputNode))) {
+ if (!subRef.compareAndSet(null, Maybe.present(outputNode))) {
// Job's already been cancelled, schedule deactivation
scheduleDeactivation(outputNode)
} else if (needsEval) {
@@ -289,21 +302,15 @@
}
private fun mutableChildBuildScope(): BuildScopeImpl {
- val stopEmitter = newStopEmitter("mutableChildBuildScope")
val childScope = coroutineScope.childScope()
- childScope.coroutineContext.job.invokeOnCompletion { stopEmitter.emit(Unit) }
- // Ensure that once this transaction is done, the new child scope enters the completing
- // state (kept alive so long as there are child jobs).
- // TODO: need to keep the scope alive if it's used to accumulate state.
- // Otherwise, stopEmitter will emit early, due to the call to complete().
- // scheduleOutput(
- // OneShot {
- // // TODO: don't like this cast
- // (childScope.coroutineContext.job as CompletableJob).complete()
- // }
- // )
+ val stopEmitter = lazy {
+ newStopEmitter("mutableChildBuildScope").apply {
+ childScope.invokeOnCancel { emit(Unit) }
+ }
+ }
return BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter),
+ stateScope =
+ StateScopeImpl(evalScope = stateScope.evalScope, endSignalLazy = stopEmitter),
coroutineScope = childScope,
)
}
@@ -314,6 +321,7 @@
coroutineScope: CoroutineScope,
) =
BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal),
+ stateScope =
+ StateScopeImpl(evalScope = this, endSignalLazy = outerScope.stateScope.endSignalLazy),
coroutineScope,
)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
index 9496b06..f86e761 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
@@ -19,16 +19,14 @@
import com.android.systemui.kairos.internal.store.Single
import com.android.systemui.kairos.internal.store.SingletonMapK
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
-internal inline fun <A> filterJustImpl(
+internal inline fun <A> filterPresentImpl(
crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>>
): EventsImpl<A> =
DemuxImpl(
mapImpl(getPulse) { maybeResult, _ ->
- if (maybeResult is Just) {
+ if (maybeResult is Present) {
Single(maybeResult.value)
} else {
Single<A>()
@@ -43,6 +41,7 @@
crossinline getPulse: EvalScope.() -> EventsImpl<A>,
crossinline f: EvalScope.(A) -> Boolean,
): EventsImpl<A> {
- val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached()
- return filterJustImpl { mapped }
+ val mapped =
+ mapImpl(getPulse) { it, _ -> if (f(it)) Maybe.present(it) else Maybe.absent }.cached()
+ return filterPresentImpl { mapped }
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
index 8a3e01a..9b4778a 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
@@ -17,11 +17,10 @@
package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.store.StoreEntry
+import com.android.systemui.kairos.util.MapPatch
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.applyPatch
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.toMaybe
internal class IncrementalImpl<K, out V>(
name: String?,
@@ -48,12 +47,11 @@
val store = StateSource(init)
val maybeChanges =
mapImpl(getPatches) { patch, _ ->
- val (old, _) = store.getCurrentWithEpoch(evalScope = this)
- val new = old.applyPatch(patch)
- if (new != old) just(patch to new) else none
+ val (current, _) = store.getCurrentWithEpoch(evalScope = this)
+ current.applyPatchCalm(patch).toMaybe()
}
.cached()
- val calm = filterJustImpl { maybeChanges }
+ val calm = filterPresentImpl { maybeChanges }
val changes = mapImpl({ calm }) { (_, change), _ -> change }
val patches = mapImpl({ calm }) { (patch, _), _ -> patch }
evalScope.scheduleOutput(
@@ -70,22 +68,44 @@
return IncrementalImpl(name, operatorName, changes, patches, store)
}
+private fun <K, V> Map<K, V>.applyPatchCalm(
+ patch: MapPatch<K, V>
+): Pair<MapPatch<K, V>, Map<K, V>>? {
+ val current = this
+ val filteredPatch = mutableMapOf<K, Maybe<V>>()
+ val new = current.toMutableMap()
+ for ((key, change) in patch) {
+ when (change) {
+ is Maybe.Present -> {
+ if (key !in current || current.getValue(key) != change.value) {
+ filteredPatch[key] = change
+ new[key] = change.value
+ }
+ }
+ Maybe.Absent -> {
+ if (key in current) {
+ filteredPatch[key] = change
+ new.remove(key)
+ }
+ }
+ }
+ }
+ return if (filteredPatch.isNotEmpty()) filteredPatch to new else null
+}
+
internal inline fun <K, V> EventsImpl<Map<K, Maybe<V>>>.calmUpdates(
state: StateDerived<Map<K, V>>
): Pair<EventsImpl<Map<K, Maybe<V>>>, EventsImpl<Map<K, V>>> {
val maybeUpdate =
mapImpl({ this@calmUpdates }) { patch, _ ->
val (current, _) = state.getCurrentWithEpoch(evalScope = this)
- val new = current.applyPatch(patch)
- if (new != current) {
- state.setCacheFromPush(new, epoch)
- just(patch to new)
- } else {
- none
- }
+ current
+ .applyPatchCalm(patch)
+ ?.also { (_, newMap) -> state.setCacheFromPush(newMap, epoch) }
+ .toMaybe()
}
.cached()
- val calm = filterJustImpl { maybeUpdate }
+ val calm = filterPresentImpl { maybeUpdate }
val patches = mapImpl({ calm }) { (p, _), _ -> p }
val changes = mapImpl({ calm }) { (_, s), _ -> s }
return patches to changes
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
index 640c561..4fa1070 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
@@ -17,8 +17,6 @@
package com.android.systemui.kairos.internal
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
import kotlinx.coroutines.ExperimentalCoroutinesApi
/** Performs actions once, when the reactive component is first connected to the network. */
@@ -44,9 +42,9 @@
@OptIn(ExperimentalCoroutinesApi::class)
fun getUnsafe(): Maybe<A> =
if (cache.isInitialized()) {
- just(cache.value.second)
+ Maybe.present(cache.value.second)
} else {
- none
+ Maybe.absent
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
index cf74f75..c11eb12 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
@@ -28,14 +28,13 @@
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import com.android.systemui.kairos.util.These
import com.android.systemui.kairos.util.flatMap
import com.android.systemui.kairos.util.getMaybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.maybeThat
-import com.android.systemui.kairos.util.maybeThis
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
import com.android.systemui.kairos.util.merge
import com.android.systemui.kairos.util.orError
import com.android.systemui.kairos.util.these
@@ -133,8 +132,8 @@
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
+ is Present -> adds.add(k to newUpstream.value)
+ Absent -> removes.add(k)
}
}
@@ -282,7 +281,8 @@
crossinline getStorage: EvalScope.() -> EventsImpl<A>,
crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
): EventsImpl<A> {
- val patches = mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+ val patches =
+ mapImpl(getPatches) { newEvents, _ -> singleOf(Maybe.present(newEvents)).asIterable() }
val switchDeferredImpl =
switchDeferredImpl(
name = name,
@@ -402,8 +402,8 @@
): EventsImpl<These<A, B>> {
val storage =
listOf(
- mapImpl(getPulse) { it, _ -> These.thiz(it) },
- mapImpl(getOther) { it, _ -> These.that(it) },
+ mapImpl(getPulse) { it, _ -> These.first(it) },
+ mapImpl(getOther) { it, _ -> These.second(it) },
)
.asIterableWithIndex()
val switchNode =
@@ -417,9 +417,9 @@
mapImpl({ switchNode }) { it, logIndent ->
val mergeResults = it.asArrayHolder()
val first =
- mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() }
+ mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeFirst() }
val second =
- mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() }
+ mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeSecond() }
these(first, second).orError { "unexpected missing merge result" }
}
return merged.cached()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
index 32aef5c..cb2c6e5 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
@@ -24,9 +24,8 @@
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
internal class MuxPromptNode<W, K, V>(
val name: String?,
@@ -94,8 +93,8 @@
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
+ is Present -> adds.add(k to newUpstream.value)
+ Absent -> removes.add(k)
}
}
@@ -311,7 +310,9 @@
switchPromptImpl(
getStorage = { singleOf(getStorage()).asIterable() },
getPatches = {
- mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+ mapImpl(getPatches) { newEvents, _ ->
+ singleOf(Maybe.present(newEvents)).asIterable()
+ }
},
storeFactory = SingletonMapK.Factory(),
)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
index fbc2b364..6e86dd1 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
@@ -21,9 +21,7 @@
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.ContinuationInterceptor
import kotlin.time.measureTime
@@ -33,6 +31,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@@ -148,6 +147,10 @@
/** Evaluates [block] inside of a new transaction when the network is ready. */
fun <R> transaction(reason: String, block: suspend EvalScope.() -> R): Deferred<R> =
CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult ->
+ if (!coroutineScope.isActive) {
+ onResult.cancel()
+ return@also
+ }
val job =
coroutineScope.launch {
inputScheduleChan.send(
@@ -261,25 +264,25 @@
private val onResult: CompletableDeferred<T>? = null,
private val onStartTransaction: suspend EvalScope.() -> T,
) {
- private var result: Maybe<T> = none
+ private var result: Maybe<T> = Maybe.absent
suspend fun started(evalScope: EvalScope) {
- result = just(onStartTransaction(evalScope))
+ result = Maybe.present(onStartTransaction(evalScope))
}
fun fail(ex: Exception) {
- result = none
+ result = Maybe.absent
onResult?.completeExceptionally(ex)
}
fun completed() {
if (onResult != null) {
when (val result = result) {
- is Just -> onResult.complete(result.value)
+ is Present -> onResult.complete(result.value)
else -> {}
}
}
- result = none
+ result = Maybe.absent
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
index 46127cb2..da83258 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
@@ -22,8 +22,6 @@
import com.android.systemui.kairos.internal.store.StoreEntry
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
internal open class StateImpl<out A>(
val name: String?,
@@ -73,7 +71,7 @@
fun getCachedUnsafe(): Maybe<A> {
@Suppress("UNCHECKED_CAST")
- return if (cache == EmptyCache) none else just(cache as A)
+ return if (cache == EmptyCache) Maybe.absent else Maybe.present(cache as A)
}
protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>?
@@ -117,7 +115,8 @@
override fun toString(): String = "StateImpl(current=$_current, writeEpoch=$writeEpoch)"
- fun getStorageUnsafe(): Maybe<S> = if (_current.isInitialized()) just(_current.value) else none
+ fun getStorageUnsafe(): Maybe<S> =
+ if (_current.isInitialized()) Maybe.present(_current.value) else Maybe.absent
}
internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
index bd1f94f..53a704a 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
@@ -36,10 +36,14 @@
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.map
-internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) :
+internal class StateScopeImpl(val evalScope: EvalScope, val endSignalLazy: Lazy<Events<Any>>) :
InternalStateScope, EvalScope by evalScope {
- override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+ override val endSignal: Events<Any> by endSignalLazy
+
+ override val endSignalOnce: Events<Any> by lazy {
+ endSignal.nextOnlyInternal("StateScope.endSignal")
+ }
override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> =
DeferredValue(deferAsync { block() })
@@ -119,7 +123,7 @@
}
override fun childStateScope(newEnd: Events<Any>) =
- StateScopeImpl(evalScope, merge(newEnd, endSignal))
+ StateScopeImpl(evalScope, lazy { merge(newEnd, endSignal) })
private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> =
if (endSignalOnce === emptyEvents) {
@@ -165,4 +169,4 @@
}
private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
- StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal)
+ StateScopeImpl(evalScope = this, endSignalLazy = outerScope.endSignalLazy)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
index 9b6940d..c34e67e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
@@ -17,8 +17,7 @@
package com.android.systemui.kairos.internal.util
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
import java.util.concurrent.ConcurrentHashMap
private object NULL
@@ -32,7 +31,7 @@
@Suppress("UNCHECKED_CAST")
operator fun <A> get(key: Key<A>): Maybe<A> =
- store[key]?.let { just((if (it === NULL) null else it) as A) } ?: None
+ store[key]?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
operator fun <A> set(key: Key<A>, value: A) {
store[key] = value ?: NULL
@@ -57,7 +56,7 @@
@Suppress("UNCHECKED_CAST")
fun <A> remove(key: Key<A>): Maybe<A> =
- store.remove(key)?.let { just((if (it === NULL) null else it) as A) } ?: None
+ store.remove(key)?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
@Suppress("UNCHECKED_CAST")
fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
index 466a9f8..d2a169c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
@@ -112,7 +112,7 @@
}
}
-internal fun CoroutineScope.launchOnCancel(
+internal fun CoroutineScope.invokeOnCancel(
context: CoroutineContext = EmptyCoroutineContext,
block: () -> Unit,
): Job =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
index 957d46f..9f17d56 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
@@ -18,100 +18,118 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
/**
- * Contains a value of two possibilities: `Left<A>` or `Right<B>`
+ * Contains a value of two possibilities: `First<A>` or `Second<B>`
*
* [Either] generalizes sealed classes the same way that [Pair] generalizes data classes; if a
* [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous
* set of two options.
*/
sealed interface Either<out A, out B> {
- /** An [Either] that contains a [Left] value. */
- @JvmInline value class Left<out A>(val value: A) : Either<A, Nothing>
+ /** An [Either] that contains a [First] value. */
+ @JvmInline value class First<out A>(val value: A) : Either<A, Nothing>
- /** An [Either] that contains a [Right] value. */
- @JvmInline value class Right<out B>(val value: B) : Either<Nothing, B>
+ /** An [Either] that contains a [Second] value. */
+ @JvmInline value class Second<out B>(val value: B) : Either<Nothing, B>
+
+ companion object {
+ /** Constructs an [Either] containing the first possibility. */
+ fun <A> first(value: A): Either<A, Nothing> = First(value)
+
+ /** Constructs a [Either] containing the second possibility. */
+ fun <B> second(value: B): Either<Nothing, B> = Second(value)
+ }
}
/**
- * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the
- * [Right] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [First] value, or the
+ * [Second] value unchanged.
*/
-inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> =
+inline fun <A, B, C> Either<A, C>.mapFirst(transform: (A) -> B): Either<B, C> =
when (this) {
- is Left -> Left(transform(value))
- is Right -> this
+ is First -> First(transform(value))
+ is Second -> this
}
/**
- * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the
- * [Left] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [Second] value, or the
+ * [First] value unchanged.
*/
-inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> =
+inline fun <A, B, C> Either<A, B>.mapSecond(transform: (B) -> C): Either<A, C> =
when (this) {
- is Left -> this
- is Right -> Right(transform(value))
+ is First -> this
+ is Second -> Second(transform(value))
}
-/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */
-inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> =
+/** Returns a [Maybe] containing the [First] value held by this [Either], if present. */
+inline fun <A> Either<A, *>.firstMaybe(): Maybe<A> =
when (this) {
- is Left -> just(value)
- else -> none
+ is First -> Maybe.present(value)
+ else -> Maybe.absent
}
-/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */
-inline fun <A> Either<A, *>.leftOrNull(): A? =
+/** Returns the [First] value held by this [Either], or `null` if this is a [Second] value. */
+inline fun <A> Either<A, *>.firstOrNull(): A? =
when (this) {
- is Left -> value
+ is First -> value
else -> null
}
-/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */
-inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> =
+/** Returns a [Maybe] containing the [Second] value held by this [Either], if present. */
+inline fun <B> Either<*, B>.secondMaybe(): Maybe<B> =
when (this) {
- is Right -> just(value)
- else -> none
+ is Second -> Maybe.present(value)
+ else -> Maybe.absent
}
-/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */
-inline fun <B> Either<*, B>.rightOrNull(): B? =
+/** Returns the [Second] value held by this [Either], or `null` if this is a [First] value. */
+inline fun <B> Either<*, B>.secondOrNull(): B? =
when (this) {
- is Right -> value
+ is Second -> value
else -> null
}
/**
- * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and
- * [Pair.second] contains all [Right] values.
+ * Returns a [These] containing either the [First] value as [These.first], or the [Second] value as
+ * [These.second]. Will never return a [These.both].
+ */
+fun <A, B> Either<A, B>.asThese(): These<A, B> =
+ when (this) {
+ is Second -> These.second(value)
+ is First -> These.first(value)
+ }
+
+/**
+ * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
*/
fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> {
- val lefts = mutableListOf<A>()
- val rights = mutableListOf<B>()
+ val firsts = mutableListOf<A>()
+ val seconds = mutableListOf<B>()
for (either in this) {
when (either) {
- is Left -> lefts.add(either.value)
- is Right -> rights.add(either.value)
+ is First -> firsts.add(either.value)
+ is Second -> seconds.add(either.value)
}
}
- return lefts to rights
+ return firsts to seconds
}
/**
- * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values,
- * and [Pair.second] contains all [Right] values.
+ * Partitions this map of [Either] values into two maps; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
*/
fun <K, A, B> Map<K, Either<A, B>>.partitionEithers(): Pair<Map<K, A>, Map<K, B>> {
- val lefts = mutableMapOf<K, A>()
- val rights = mutableMapOf<K, B>()
+ val firsts = mutableMapOf<K, A>()
+ val seconds = mutableMapOf<K, B>()
for ((k, e) in this) {
when (e) {
- is Left -> lefts[k] = e.value
- is Right -> rights[k] = e.value
+ is First -> firsts[k] = e.value
+ is Second -> seconds[k] = e.value
}
}
- return lefts to rights
+ return firsts to seconds
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
index f368cbf..8fe41bc 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
@@ -16,9 +16,9 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
+import com.android.systemui.kairos.util.Maybe.Present
/** A "patch" that can be used to batch-update a [Map], via [applyPatch]. */
typealias MapPatch<K, V> = Map<K, Maybe<V>>
@@ -27,16 +27,16 @@
* Returns a new [Map] that has [patch] applied to the original map.
*
* For each entry in [patch]:
- * * a [Just] value will be included in the new map, replacing the entry in the original map with
+ * * a [Present] value will be included in the new map, replacing the entry in the original map with
* the same key, if present.
- * * a [Maybe.None] value will be omitted from the new map, excluding the entry in the original map
- * with the same key, if present.
+ * * a [Maybe.Absent] value will be omitted from the new map, excluding the entry in the original
+ * map with the same key, if present.
*/
fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> {
val (adds: List<Pair<K, V>>, removes: List<K>) =
patch
.asSequence()
- .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) }
+ .map { (k, v) -> if (v is Present) First(k to v.value) else Second(k) }
.partitionEithers()
val removed: Map<K, V> = this - removes.toSet()
val updated: Map<K, V> = removed + adds
@@ -47,11 +47,11 @@
* Returns a [MapPatch] that, when applied, includes all of the values from the original [Map].
*
* Shorthand for:
- * ```kotlin
- * mapValues { just(it.value) }
+ * ``` kotlin
+ * mapValues { (key, value) -> Maybe.present(value) }
* ```
*/
-fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { just(it.value) }
+fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { Maybe.present(it.value) }
/**
* Returns a [MapPatch] that, when applied, includes all of the entries from [new] whose keys are
@@ -67,10 +67,10 @@
val adds = new - old.keys
return buildMap {
for (removed in removes) {
- put(removed, none)
+ put(removed, Maybe.absent)
}
for ((newKey, newValue) in adds) {
- put(newKey, just(newValue))
+ put(newKey, Maybe.present(newValue))
}
}
}
@@ -86,13 +86,16 @@
*/
fun <K, V> mapPatchFromFullDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
val removes = old.keys - new.keys
- val adds = new.mapMaybeValues { (k, v) -> if (k in old && v == old[k]) none else just(v) }
+ val adds =
+ new.mapMaybeValues { (k, v) ->
+ if (k in old && v == old[k]) Maybe.absent else Maybe.present(v)
+ }
return hashMapOf<K, Maybe<V>>().apply {
for (removed in removes) {
- put(removed, none)
+ put(removed, Maybe.absent)
}
for ((newKey, newValue) in adds) {
- put(newKey, just(newValue))
+ put(newKey, Maybe.present(newValue))
}
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
index 681218399..4754bc4 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
@@ -18,8 +18,8 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -31,17 +31,28 @@
/** Represents a value that may or may not be present. */
sealed interface Maybe<out A> {
/** A [Maybe] value that is present. */
- @JvmInline value class Just<out A> internal constructor(val value: A) : Maybe<A>
+ @JvmInline value class Present<out A> internal constructor(val value: A) : Maybe<A>
/** A [Maybe] value that is not present. */
- data object None : Maybe<Nothing>
+ data object Absent : Maybe<Nothing>
+
+ companion object {
+ /** Returns a [Maybe] containing [value]. */
+ fun <A> present(value: A): Maybe<A> = Present(value)
+
+ /** A [Maybe] that is not present. */
+ val absent: Maybe<Nothing> = Absent
+
+ /** A [Maybe] that is not present. */
+ inline fun <A> absent(): Maybe<A> = Absent
+ }
}
/** Utilities to query [Maybe] instances from within a [maybe] block. */
@RestrictsSuspension
object MaybeScope {
suspend operator fun <A> Maybe<A>.not(): A = suspendCoroutine { k ->
- if (this is Just) k.resume(value)
+ if (this is Present) k.resume(value)
}
suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k ->
@@ -53,7 +64,8 @@
* Returns a [Maybe] value produced by evaluating [block].
*
* [block] can use its [MaybeScope] receiver to query other [Maybe] values, automatically cancelling
- * execution of [block] and producing [None] when attempting to query a [Maybe] that is not present.
+ * execution of [block] and producing [Absent] when attempting to query a [Maybe] that is not
+ * present.
*
* This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing
* with complex combinations of nullables:
@@ -68,33 +80,30 @@
* ```
*/
fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> {
- var maybeResult: Maybe<A> = None
+ var maybeResult: Maybe<A> = Absent
val k =
object : Continuation<A> {
override val context: CoroutineContext = EmptyCoroutineContext
override fun resumeWith(result: Result<A>) {
- maybeResult = result.getOrNull()?.let { just(it) } ?: None
+ maybeResult = result.getOrNull()?.let { Maybe.present(it) } ?: Absent
}
}
block.startCoroutine(MaybeScope, k)
return maybeResult
}
-/** Returns a [Just] containing this value, or [None] if `null`. */
+/** Returns a [Maybe] containing this value if it is not `null`. */
inline fun <A> (A?).toMaybe(): Maybe<A> = maybe(this)
-/** Returns a [Just] containing a non-null [value], or [None] if `null`. */
-inline fun <A> maybe(value: A?): Maybe<A> = value?.let(::just) ?: None
+/** Returns a [Maybe] containing [value] if it is not `null`. */
+inline fun <A> maybe(value: A?): Maybe<A> = value?.let { Maybe.present(it) } ?: Absent
-/** Returns a [Just] containing [value]. */
-fun <A> just(value: A): Maybe<A> = Just(value)
+/** Returns a [Maybe] that is absent. */
+fun <A> maybeOf(): Maybe<A> = Absent
-/** A [Maybe] that is not present. */
-val none: Maybe<Nothing> = None
-
-/** A [Maybe] that is not present. */
-inline fun <A> none(): Maybe<A> = None
+/** Returns a [Maybe] containing [value]. */
+fun <A> maybeOf(value: A): Maybe<A> = Present(value)
/** Returns the value present in this [Maybe], or `null` if not present. */
inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
@@ -105,22 +114,22 @@
*/
inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> =
when (this) {
- is Just -> just(transform(value))
- is None -> None
+ is Present -> Maybe.present(transform(value))
+ is Absent -> Absent
}
/** Returns the result of applying [transform] to the value in the original [Maybe]. */
inline fun <A, B> Maybe<A>.flatMap(transform: (A) -> Maybe<B>): Maybe<B> =
when (this) {
- is Just -> transform(value)
- is None -> None
+ is Present -> transform(value)
+ is Absent -> Absent
}
/** Returns the value present in this [Maybe], or the result of [defaultValue] if not present. */
inline fun <A> Maybe<A>.orElseGet(defaultValue: () -> A): A =
when (this) {
- is Just -> value
- is None -> defaultValue()
+ is Present -> value
+ is Absent -> defaultValue()
}
/**
@@ -132,8 +141,8 @@
/** Returns the value present in this [Maybe], or [defaultValue] if not present. */
inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
when (this) {
- is Just -> value
- is None -> defaultValue
+ is Present -> value
+ is Absent -> defaultValue
}
/**
@@ -142,15 +151,16 @@
*/
inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> =
when (this) {
- is Just -> if (predicate(value)) this else None
+ is Present -> if (predicate(value)) this else Absent
else -> this
}
/** Returns a [List] containing all values that are present in this [Iterable]. */
-fun <A> Iterable<Maybe<A>>.filterJust(): List<A> = asSequence().filterJust().toList()
+fun <A> Iterable<Maybe<A>>.filterPresent(): List<A> = asSequence().filterPresent().toList()
/** Returns a [List] containing all values that are present in this [Sequence]. */
-fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>().map { it.value }
+fun <A> Sequence<Maybe<A>>.filterPresent(): Sequence<A> =
+ filterIsInstance<Present<A>>().map { it.value }
// Align
@@ -160,23 +170,25 @@
*/
inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> =
when (this) {
- is Just -> {
+ is Present -> {
val a = value
when (other) {
- is Just -> {
+ is Present -> {
val b = other.value
- just(transform(These.both(a, b)))
+ Maybe.present(transform(These.both(a, b)))
}
- None -> just(transform(These.thiz(a)))
+
+ Absent -> Maybe.present(transform(These.first(a)))
}
}
- None ->
+ Absent ->
when (other) {
- is Just -> {
+ is Present -> {
val b = other.value
- just(transform(These.that(b)))
+ Maybe.present(transform(These.second(b)))
}
- None -> none
+
+ Absent -> Maybe.absent
}
}
@@ -190,7 +202,7 @@
*/
inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> =
when (this) {
- is Just -> this
+ is Present -> this
else -> other()
}
@@ -235,7 +247,7 @@
inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = buildList {
for (a in this@mapMaybe) {
val result = transform(a)
- if (result is Just) {
+ if (result is Present) {
add(result.value)
}
}
@@ -246,7 +258,7 @@
* the original sequence.
*/
fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> =
- map(transform).filterIsInstance<Just<B>>().map { it.value }
+ map(transform).filterIsInstance<Present<B>>().map { it.value }
/**
* Returns a map with values of only the present results of applying [transform] to each entry in
@@ -256,14 +268,14 @@
buildMap {
for (entry in this@mapMaybeValues) {
val result = transform(entry)
- if (result is Just) {
+ if (result is Present) {
put(entry.key, result.value)
}
}
}
/** Returns a map with all non-present values filtered out. */
-fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> =
+fun <K, A> Map<K, Maybe<A>>.filterPresentValues(): Map<K, A> =
asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap()
/**
@@ -277,9 +289,9 @@
fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> {
val value = get(key)
if (value == null && !containsKey(key)) {
- return none
+ return Maybe.absent
} else {
@Suppress("UNCHECKED_CAST")
- return just(value as V)
+ return Maybe.present(value as V)
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
index 092dca4..fc7b1e0 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
@@ -16,28 +16,28 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Maybe.Present
/** Contains at least one of two potential values. */
sealed class These<out A, out B> {
- /** Contains a single potential value. */
- class This<A, B> internal constructor(val thiz: A) : These<A, B>()
+ /** A [These] that contains a [First] value. */
+ class First<A, B> internal constructor(val value: A) : These<A, B>()
- /** Contains a single potential value. */
- class That<A, B> internal constructor(val that: B) : These<A, B>()
+ /** A [These] that contains a [Second] value. */
+ class Second<A, B> internal constructor(val value: B) : These<A, B>()
- /** Contains both potential values. */
- class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>()
+ /** A [These] that contains [Both] a [first] and [second] value. */
+ class Both<A, B> internal constructor(val first: A, val second: B) : These<A, B>()
companion object {
- /** Constructs a [These] containing only [thiz]. */
- fun <A> thiz(thiz: A): These<A, Nothing> = This(thiz)
+ /** Constructs a [These] containing the first possibility. */
+ fun <A> first(value: A): These<A, Nothing> = First(value)
- /** Constructs a [These] containing only [that]. */
- fun <B> that(that: B): These<Nothing, B> = That(that)
+ /** Constructs a [These] containing the second possibility. */
+ fun <B> second(value: B): These<Nothing, B> = Second(value)
- /** Constructs a [These] containing both [thiz] and [that]. */
- fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that)
+ /** Constructs a [These] containing both possibilities. */
+ fun <A, B> both(first: A, second: B): These<A, B> = Both(first, second)
}
}
@@ -47,87 +47,88 @@
*/
inline fun <A> These<A, A>.merge(f: (A, A) -> A): A =
when (this) {
- is These.This -> thiz
- is These.That -> that
- is These.Both -> f(thiz, that)
+ is These.First -> value
+ is These.Second -> value
+ is These.Both -> f(first, second)
}
-/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */
-fun <A> These<A, *>.maybeThis(): Maybe<A> =
+/** Returns the [These.First] [value][These.First.value] present in this [These] as a [Maybe]. */
+fun <A> These<A, *>.maybeFirst(): Maybe<A> =
when (this) {
- is These.Both -> just(thiz)
- is These.That -> none
- is These.This -> just(thiz)
+ is These.Both -> Maybe.present(first)
+ is These.Second -> Maybe.absent
+ is These.First -> Maybe.present(value)
}
/**
- * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not
+ * Returns the [These.First] [value][These.First.value] present in this [These], or `null` if not
* present.
*/
-fun <A : Any> These<A, *>.thisOrNull(): A? =
+fun <A : Any> These<A, *>.firstOrNull(): A? =
when (this) {
- is These.Both -> thiz
- is These.That -> null
- is These.This -> thiz
+ is These.Both -> first
+ is These.Second -> null
+ is These.First -> value
}
-/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */
-fun <A> These<*, A>.maybeThat(): Maybe<A> =
+/** Returns the [These.Second] [value][These.Second.value] present in this [These] as a [Maybe]. */
+fun <A> These<*, A>.maybeSecond(): Maybe<A> =
when (this) {
- is These.Both -> just(that)
- is These.That -> just(that)
- is These.This -> none
+ is These.Both -> Maybe.present(second)
+ is These.Second -> Maybe.present(value)
+ is These.First -> Maybe.absent
}
/**
- * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not
+ * Returns the [These.Second] [value][These.Second.value] present in this [These], or `null` if not
* present.
*/
-fun <A : Any> These<*, A>.thatOrNull(): A? =
+fun <A : Any> These<*, A>.secondOrNull(): A? =
when (this) {
- is These.Both -> that
- is These.That -> that
- is These.This -> null
+ is These.Both -> second
+ is These.Second -> value
+ is These.First -> null
}
/** Returns [These.Both] values present in this [These] as a [Maybe]. */
fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> =
when (this) {
- is These.Both -> just(thiz to that)
- else -> none
+ is These.Both -> Maybe.present(first to second)
+ else -> Maybe.absent
}
-/** Returns a [These] containing [thiz] and/or [that] if they are present. */
-fun <A, B> these(thiz: Maybe<A>, that: Maybe<B>): Maybe<These<A, B>> =
- when (thiz) {
- is Just ->
- just(
- when (that) {
- is Just -> These.both(thiz.value, that.value)
- else -> These.thiz(thiz.value)
+/** Returns a [These] containing [first] and/or [second] if they are present. */
+fun <A, B> these(first: Maybe<A>, second: Maybe<B>): Maybe<These<A, B>> =
+ when (first) {
+ is Present ->
+ Maybe.present(
+ when (second) {
+ is Present -> These.both(first.value, second.value)
+ else -> These.first(first.value)
}
)
+
else ->
- when (that) {
- is Just -> just(These.that(that.value))
- else -> none
+ when (second) {
+ is Present -> Maybe.present(These.second(second.value))
+ else -> Maybe.absent
}
}
/**
- * Returns a [These] containing [thiz] and/or [that] if they are non-null, or `null` if both are
+ * Returns a [These] containing [first] and/or [second] if they are non-null, or `null` if both are
* `null`.
*/
-fun <A : Any, B : Any> theseNull(thiz: A?, that: B?): These<A, B>? =
- thiz?.let { that?.let { These.both(thiz, that) } ?: These.thiz(thiz) }
- ?: that?.let { These.that(that) }
+fun <A : Any, B : Any> theseNotNull(first: A?, second: B?): These<A, B>? =
+ first?.let { second?.let { These.both(first, second) } ?: These.first(first) }
+ ?: second?.let { These.second(second) }
/**
- * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second]
- * containing all [These.That] values.
+ * Returns two maps, with [Pair.first] containing all [These.First] values and [Pair.second]
+ * containing all [These.Second] values.
*
* If the value is [These.Both], then the associated key with appear in both output maps, bound to
- * [These.Both.thiz] and [These.Both.that] in each respective output.
+ * [These.Both.first] and [These.Both.second] in each respective output.
*/
fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
val a = mutableMapOf<K, A>()
@@ -135,14 +136,14 @@
for ((k, t) in this) {
when (t) {
is These.Both -> {
- a[k] = t.thiz
- b[k] = t.that
+ a[k] = t.first
+ b[k] = t.second
}
- is These.That -> {
- b[k] = t.that
+ is These.Second -> {
+ b[k] = t.value
}
- is These.This -> {
- a[k] = t.thiz
+ is These.First -> {
+ a[k] = t.value
}
}
}
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
new file mode 100644
index 0000000..88a5b7a
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2025 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.kairos
+
+import com.android.systemui.kairos.util.MapPatch
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeOf
+import com.android.systemui.kairos.util.toMaybe
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class KairosSamples {
+
+ @Test fun test_mapMaybe() = runSample { mapMaybe() }
+
+ fun BuildScope.mapMaybe() {
+ val emitter = MutableEvents<String>()
+ val ints = emitter.mapMaybe { it.toIntOrNull().toMaybe() }
+
+ var observedInput: String? = null
+ emitter.observe { observedInput = it }
+
+ var observedInt: Int? = null
+ ints.observe { observedInt = it }
+
+ launchEffect {
+ // parse succeeds
+ emitter.emit("6")
+ assertEquals(observedInput, "6")
+ assertEquals(observedInt, 6)
+
+ // parse fails
+ emitter.emit("foo")
+ assertEquals(observedInput, "foo")
+ assertEquals(observedInt, 6)
+
+ // parse succeeds
+ emitter.emit("500")
+ assertEquals(observedInput, "500")
+ assertEquals(observedInt, 500)
+ }
+ }
+
+ @Test fun test_mapCheap() = runSample { mapCheap() }
+
+ fun BuildScope.mapCheap() {
+ val emitter = MutableEvents<Int>()
+
+ var invocationCount = 0
+ val squared =
+ emitter.mapCheap {
+ invocationCount++
+ it * it
+ }
+
+ var observedSquare: Int? = null
+ squared.observe { observedSquare = it }
+
+ launchEffect {
+ emitter.emit(10)
+ assertTrue(invocationCount >= 1)
+ assertEquals(observedSquare, 100)
+
+ emitter.emit(2)
+ assertTrue(invocationCount >= 2)
+ assertEquals(observedSquare, 4)
+ }
+ }
+
+ @Test fun test_mapEvents() = runSample { mapEvents() }
+
+ fun BuildScope.mapEvents() {
+ val emitter = MutableEvents<Int>()
+
+ val squared = emitter.map { it * it }
+
+ var observedSquare: Int? = null
+ squared.observe { observedSquare = it }
+
+ launchEffect {
+ emitter.emit(10)
+ assertEquals(observedSquare, 100)
+
+ emitter.emit(2)
+ assertEquals(observedSquare, 4)
+ }
+ }
+
+ @Test fun test_eventsLoop() = runSample { eventsLoop() }
+
+ fun BuildScope.eventsLoop() {
+ val emitter = MutableEvents<Unit>()
+ var newCount: Events<Int> by EventsLoop()
+ val count = newCount.holdState(0)
+ newCount = emitter.map { count.sample() + 1 }
+
+ var observedCount = 0
+ count.observe { observedCount = it }
+
+ launchEffect {
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 1)
+
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 2)
+ }
+ }
+
+ @Test fun test_stateLoop() = runSample { stateLoop() }
+
+ fun BuildScope.stateLoop() {
+ val emitter = MutableEvents<Unit>()
+ var count: State<Int> by StateLoop()
+ count = emitter.map { count.sample() + 1 }.holdState(0)
+
+ var observedCount = 0
+ count.observe { observedCount = it }
+
+ launchEffect {
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 1)
+
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 2)
+ }
+ }
+
+ @Test fun test_changes() = runSample { changes() }
+
+ fun BuildScope.changes() {
+ val emitter = MutableEvents<Int>()
+ val state = emitter.holdState(0)
+
+ var numEmissions = 0
+ emitter.observe { numEmissions++ }
+
+ var observedState = 0
+ var numChangeEmissions = 0
+ state.changes.observe {
+ observedState = it
+ numChangeEmissions++
+ }
+
+ launchEffect {
+ emitter.emit(0)
+ assertEquals(numEmissions, expected = 1)
+ assertEquals(numChangeEmissions, expected = 0)
+ assertEquals(observedState, expected = 0)
+
+ emitter.emit(5)
+ assertEquals(numEmissions, expected = 2)
+ assertEquals(numChangeEmissions, expected = 1)
+ assertEquals(observedState, expected = 5)
+
+ emitter.emit(3)
+ assertEquals(numEmissions, expected = 3)
+ assertEquals(numChangeEmissions, expected = 2)
+ assertEquals(observedState, expected = 3)
+
+ emitter.emit(3)
+ assertEquals(numEmissions, expected = 4)
+ assertEquals(numChangeEmissions, expected = 2)
+ assertEquals(observedState, expected = 3)
+
+ emitter.emit(5)
+ assertEquals(numEmissions, expected = 5)
+ assertEquals(numChangeEmissions, expected = 3)
+ assertEquals(observedState, expected = 5)
+ }
+ }
+
+ @Test fun test_partitionThese() = runSample { partitionThese() }
+
+ fun BuildScope.partitionThese() {
+ val emitter = MutableEvents<These<Int, String>>()
+ val (lefts, rights) = emitter.partitionThese()
+
+ var observedLeft: Int? = null
+ lefts.observe { observedLeft = it }
+
+ var observedRight: String? = null
+ rights.observe { observedRight = it }
+
+ launchEffect {
+ emitter.emit(These.first(10))
+ assertEquals(observedLeft, 10)
+ assertEquals(observedRight, null)
+
+ emitter.emit(These.both(2, "foo"))
+ assertEquals(observedLeft, 2)
+ assertEquals(observedRight, "foo")
+
+ emitter.emit(These.second("bar"))
+ assertEquals(observedLeft, 2)
+ assertEquals(observedRight, "bar")
+ }
+ }
+
+ @Test fun test_merge() = runSample { merge() }
+
+ fun BuildScope.merge() {
+ val emitter = MutableEvents<Int>()
+ val fizz = emitter.mapNotNull { if (it % 3 == 0) "Fizz" else null }
+ val buzz = emitter.mapNotNull { if (it % 5 == 0) "Buzz" else null }
+ val fizzbuzz = fizz.mergeWith(buzz) { _, _ -> "Fizz Buzz" }
+ val output = mergeLeft(fizzbuzz, emitter.mapCheap { it.toString() })
+
+ var observedOutput: String? = null
+ output.observe { observedOutput = it }
+
+ launchEffect {
+ emitter.emit(1)
+ assertEquals(observedOutput, "1")
+ emitter.emit(2)
+ assertEquals(observedOutput, "2")
+ emitter.emit(3)
+ assertEquals(observedOutput, "Fizz")
+ emitter.emit(4)
+ assertEquals(observedOutput, "4")
+ emitter.emit(5)
+ assertEquals(observedOutput, "Buzz")
+ emitter.emit(6)
+ assertEquals(observedOutput, "Fizz")
+ emitter.emit(15)
+ assertEquals(observedOutput, "Fizz Buzz")
+ }
+ }
+
+ @Test fun test_groupByKey() = runSample { groupByKey() }
+
+ fun BuildScope.groupByKey() {
+ val emitter = MutableEvents<Map<String, Int>>()
+ val grouped = emitter.groupByKey()
+ val groupA = grouped["A"]
+ val groupB = grouped["B"]
+
+ var numEmissions = 0
+ emitter.observe { numEmissions++ }
+
+ var observedA: Int? = null
+ groupA.observe { observedA = it }
+
+ var observedB: Int? = null
+ groupB.observe { observedB = it }
+
+ launchEffect {
+ // emit to group A
+ emitter.emit(mapOf("A" to 3))
+ assertEquals(numEmissions, 1)
+ assertEquals(observedA, 3)
+ assertEquals(observedB, null)
+
+ // emit to groups B and C, even though there are no observers of C
+ emitter.emit(mapOf("B" to 9, "C" to 100))
+ assertEquals(numEmissions, 2)
+ assertEquals(observedA, 3)
+ assertEquals(observedB, 9)
+
+ // emit to groups A and B
+ emitter.emit(mapOf("B" to 6, "A" to 14))
+ assertEquals(numEmissions, 3)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+
+ // emit to group with no listeners
+ emitter.emit(mapOf("Q" to -66))
+ assertEquals(numEmissions, 4)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+
+ // no-op emission
+ emitter.emit(emptyMap())
+ assertEquals(numEmissions, 5)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+ }
+ }
+
+ @Test fun test_switchEvents() = runSample { switchEvents() }
+
+ fun BuildScope.switchEvents() {
+ val negator = MutableEvents<Unit>()
+ val emitter = MutableEvents<Int>()
+ val negate = negator.foldState(false) { _, negate -> !negate }
+ val output =
+ negate.map { negate -> if (negate) emitter.map { it * -1 } else emitter }.switchEvents()
+
+ var observed: Int? = null
+ output.observe { observed = it }
+
+ launchEffect {
+ // emit like normal
+ emitter.emit(10)
+ assertEquals(observed, 10)
+
+ // enable negation
+ observed = null
+ negator.emit(Unit)
+ assertEquals(observed, null)
+
+ emitter.emit(99)
+ assertEquals(observed, -99)
+
+ // disable negation
+ observed = null
+ negator.emit(Unit)
+ emitter.emit(7)
+ assertEquals(observed, 7)
+ }
+ }
+
+ @Test fun test_switchEventsPromptly() = runSample { switchEventsPromptly() }
+
+ fun BuildScope.switchEventsPromptly() {
+ val emitter = MutableEvents<Int>()
+ val enabled = emitter.map { it > 10 }.holdState(false)
+ val switchedIn = enabled.map { enabled -> if (enabled) emitter else emptyEvents }
+ val deferredSwitch = switchedIn.switchEvents()
+ val promptSwitch = switchedIn.switchEventsPromptly()
+
+ var observedDeferred: Int? = null
+ deferredSwitch.observe { observedDeferred = it }
+
+ var observedPrompt: Int? = null
+ promptSwitch.observe { observedPrompt = it }
+
+ launchEffect {
+ emitter.emit(3)
+ assertEquals(observedDeferred, null)
+ assertEquals(observedPrompt, null)
+
+ emitter.emit(20)
+ assertEquals(observedDeferred, null)
+ assertEquals(observedPrompt, 20)
+
+ emitter.emit(30)
+ assertEquals(observedDeferred, 30)
+ assertEquals(observedPrompt, 30)
+
+ emitter.emit(8)
+ assertEquals(observedDeferred, 8)
+ assertEquals(observedPrompt, 8)
+
+ emitter.emit(1)
+ assertEquals(observedDeferred, 8)
+ assertEquals(observedPrompt, 8)
+ }
+ }
+
+ @Test fun test_sampleTransactional() = runSample { sampleTransactional() }
+
+ fun BuildScope.sampleTransactional() {
+ var store = 0
+ val transactional = transactionally { store++ }
+
+ effect {
+ assertEquals(store, 0)
+ assertEquals(transactional.sample(), 0)
+ assertEquals(store, 1)
+ assertEquals(transactional.sample(), 0)
+ assertEquals(store, 1)
+ }
+ }
+
+ @Test fun test_states() = runSample { states() }
+
+ fun BuildScope.states() {
+ val constantState = stateOf(10)
+ effect { assertEquals(constantState.sample(), 10) }
+
+ val mappedConstantState: State<Int> = constantState.map { it * 2 }
+ effect { assertEquals(mappedConstantState.sample(), 20) }
+
+ val emitter = MutableEvents<Int>()
+ val heldState: State<Int?> = emitter.holdState(null)
+ effect { assertEquals(heldState.sample(), null) }
+
+ var observed: Int? = null
+ var wasObserved = false
+ heldState.observe {
+ observed = it
+ wasObserved = true
+ }
+ launchEffect {
+ assertTrue(wasObserved)
+ emitter.emit(4)
+ assertEquals(observed, 4)
+ }
+
+ val combinedStates: State<Pair<Int, Int?>> =
+ combine(mappedConstantState, heldState) { a, b -> Pair(a, b) }
+
+ effect { assertEquals(combinedStates.sample(), 20 to null) }
+
+ var observedPair: Pair<Int, Int?>? = null
+ combinedStates.observe { observedPair = it }
+ launchEffect {
+ emitter.emit(12)
+ assertEquals(observedPair, 20 to 12)
+ }
+ }
+
+ @Test fun test_holdState() = runSample { holdState() }
+
+ fun BuildScope.holdState() {
+ val emitter = MutableEvents<Int>()
+ val heldState: State<Int?> = emitter.holdState(null)
+ effect { assertEquals(heldState.sample(), null) }
+
+ var observed: Int? = null
+ var wasObserved = false
+ heldState.observe {
+ observed = it
+ wasObserved = true
+ }
+ launchEffect {
+ // observation of the initial state took place immediately
+ assertTrue(wasObserved)
+
+ // state changes are also observed
+ emitter.emit(4)
+ assertEquals(observed, 4)
+
+ emitter.emit(20)
+ assertEquals(observed, 20)
+ }
+ }
+
+ @Test fun test_mapState() = runSample { mapState() }
+
+ fun BuildScope.mapState() {
+ val emitter = MutableEvents<Int>()
+ val held: State<Int> = emitter.holdState(0)
+ val squared: State<Int> = held.map { it * it }
+
+ var observed: Int? = null
+ squared.observe { observed = it }
+
+ launchEffect {
+ assertEquals(observed, 0)
+
+ emitter.emit(10)
+ assertEquals(observed, 100)
+ }
+ }
+
+ @Test fun test_combineState() = runSample { combineState() }
+
+ fun BuildScope.combineState() {
+ val emitter = MutableEvents<Int>()
+ val state = emitter.holdState(0)
+ val squared = state.map { it * it }
+ val negated = state.map { -it }
+ val combined = squared.combine(negated) { a, b -> Pair(a, b) }
+
+ val observed = mutableListOf<Pair<Int, Int>>()
+ combined.observe { observed.add(it) }
+
+ launchEffect {
+ emitter.emit(10)
+ emitter.emit(20)
+ emitter.emit(3)
+
+ assertEquals(observed, listOf(0 to 0, 100 to -10, 400 to -20, 9 to -3))
+ }
+ }
+
+ @Test fun test_flatMap() = runSample { flatMap() }
+
+ fun BuildScope.flatMap() {
+ val toggler = MutableEvents<Unit>()
+ val firstEmitter = MutableEvents<Unit>()
+ val secondEmitter = MutableEvents<Unit>()
+
+ val firstCount: State<Int> = firstEmitter.foldState(0) { _, count -> count + 1 }
+ val secondCount: State<Int> = secondEmitter.foldState(0) { _, count -> count + 1 }
+ val toggleState: State<Boolean> = toggler.foldState(true) { _, state -> !state }
+
+ val activeCount: State<Int> =
+ toggleState.flatMap { b -> if (b) firstCount else secondCount }
+
+ var observed: Int? = null
+ activeCount.observe { observed = it }
+
+ launchEffect {
+ assertEquals(observed, 0)
+
+ firstEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ secondEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ secondEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ toggler.emit(Unit)
+ assertEquals(observed, 2)
+
+ toggler.emit(Unit)
+ assertEquals(observed, 1)
+ }
+ }
+
+ @Test fun test_incrementals() = runSample { incrementals() }
+
+ fun BuildScope.incrementals() {
+ val patchEmitter = MutableEvents<MapPatch<String, Int>>()
+ val incremental: Incremental<String, Int> = patchEmitter.foldStateMapIncrementally()
+ val squared = incremental.mapValues { (key, value) -> value * value }
+
+ var observedUpdate: MapPatch<String, Int>? = null
+ squared.updates.observe { observedUpdate = it }
+
+ var observedState: Map<String, Int>? = null
+ squared.observe { observedState = it }
+
+ launchEffect {
+ assertEquals(observedState, emptyMap())
+ assertEquals(observedUpdate, null)
+
+ // add entry: A => 10
+ patchEmitter.emit(mapOf("A" to maybeOf(10)))
+ assertEquals(observedState, mapOf("A" to 100))
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(100)))
+
+ // update entry: A => 5
+ // add entry: B => 6
+ patchEmitter.emit(mapOf("A" to maybeOf(5), "B" to maybeOf(6)))
+ assertEquals(observedState, mapOf("A" to 25, "B" to 36))
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(25), "B" to maybeOf(36)))
+
+ // remove entry: A
+ // add entry: C => 9
+ // remove non-existent entry: F
+ patchEmitter.emit(mapOf("A" to maybeOf(), "C" to maybeOf(9), "F" to maybeOf()))
+ assertEquals(observedState, mapOf("B" to 36, "C" to 81))
+ // non-existent entry is filtered from the update
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(), "C" to maybeOf(81)))
+ }
+ }
+
+ @Test fun test_mergeEventsIncrementally() = runSample(block = mergeEventsIncrementally())
+
+ fun mergeEventsIncrementally(): BuildSpec<Unit> = buildSpec {
+ val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+ val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+ val merged: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+
+ var observed: Map<String, Int>? = null
+ merged.observe { observed = it }
+
+ launchEffect {
+ // add events entry: A
+ val emitterA = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+ emitterA.emit(100)
+ assertEquals(observed, mapOf("A" to 100))
+
+ // add events entry: B
+ val emitterB = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+ // merged emits from both A and B
+ emitterB.emit(5)
+ assertEquals(observed, mapOf("B" to 5))
+
+ emitterA.emit(20)
+ assertEquals(observed, mapOf("A" to 20))
+
+ // remove entry: A
+ patchEmitter.emit(mapOf("A" to maybeOf()))
+ emitterA.emit(0)
+ // event is not emitted now that A has been removed
+ assertEquals(observed, mapOf("A" to 20))
+
+ // but B still works
+ emitterB.emit(3)
+ assertEquals(observed, mapOf("B" to 3))
+ }
+ }
+
+ @Test
+ fun test_mergeEventsIncrementallyPromptly() =
+ runSample(block = mergeEventsIncrementallyPromptly())
+
+ fun mergeEventsIncrementallyPromptly(): BuildSpec<Unit> = buildSpec {
+ val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+ val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+ val deferredMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+ val promptMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementallyPromptly()
+
+ var observedDeferred: Map<String, Int>? = null
+ deferredMerge.observe { observedDeferred = it }
+
+ var observedPrompt: Map<String, Int>? = null
+ promptMerge.observe { observedPrompt = it }
+
+ launchEffect {
+ val emitterA = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+ emitterA.emit(100)
+ assertEquals(observedDeferred, mapOf("A" to 100))
+ assertEquals(observedPrompt, mapOf("A" to 100))
+
+ val emitterB = patchEmitter.map { 5 }
+ patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+ assertEquals(observedDeferred, mapOf("A" to 100))
+ assertEquals(observedPrompt, mapOf("B" to 5))
+ }
+ }
+
+ @Test fun test_applyLatestStateful() = runSample(block = applyLatestStateful())
+
+ fun applyLatestStateful(): BuildSpec<Unit> = buildSpec {
+ val reset = MutableEvents<Unit>()
+ val emitter = MutableEvents<Unit>()
+ val stateEvents: Events<State<Int>> =
+ reset
+ .map { statefully { emitter.foldState(0) { _, count -> count + 1 } } }
+ .applyLatestStateful()
+ val activeState: State<State<Int>?> = stateEvents.holdState(null)
+
+ launchEffect {
+ // nothing is active yet
+ kairosNetwork.transact { assertEquals(activeState.sample(), null) }
+
+ // activate the counter
+ reset.emit(Unit)
+ val firstState =
+ kairosNetwork.transact {
+ assertEquals(activeState.sample()?.sample(), 0)
+ activeState.sample()!!
+ }
+
+ // emit twice
+ emitter.emit(Unit)
+ emitter.emit(Unit)
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+ // start a new counter, disabling the old one
+ reset.emit(Unit)
+ val secondState =
+ kairosNetwork.transact {
+ assertEquals(activeState.sample()?.sample(), 0)
+ activeState.sample()!!
+ }
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+ // emit: the new counter updates, but the old one does not
+ emitter.emit(Unit)
+ kairosNetwork.transact { assertEquals(secondState.sample(), 1) }
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+ }
+ }
+
+ @Test fun test_applyLatestStatefulForKey() = runSample(block = applyLatestStatefulForKey())
+
+ fun applyLatestStatefulForKey(): BuildSpec<Unit> = buildSpec {
+ val reset = MutableEvents<String>()
+ val emitter = MutableEvents<String>()
+ val stateEvents: Events<MapPatch<String, State<Int>>> =
+ reset
+ .map { key ->
+ mapOf(
+ key to
+ maybeOf(
+ statefully {
+ emitter
+ .filter { it == key }
+ .foldState(0) { _, count -> count + 1 }
+ }
+ )
+ )
+ }
+ .applyLatestStatefulForKey()
+ val activeStatesByKey: Incremental<String, State<Int>> =
+ stateEvents.foldStateMapIncrementally(emptyMap())
+
+ launchEffect {
+ // nothing is active yet
+ kairosNetwork.transact { assertEquals(activeStatesByKey.sample(), emptyMap()) }
+
+ // activate a new entry A
+ reset.emit("A")
+ val firstStateA =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A"))
+ stateMap.getValue("A").also { assertEquals(it.sample(), 0) }
+ }
+
+ // emit twice to A
+ emitter.emit("A")
+ emitter.emit("A")
+ kairosNetwork.transact { assertEquals(firstStateA.sample(), 2) }
+
+ // active a new entry B
+ reset.emit("B")
+ val firstStateB =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A", "B"))
+ stateMap.getValue("B").also {
+ assertEquals(it.sample(), 0)
+ assertEquals(firstStateA.sample(), 2)
+ }
+ }
+
+ // emit once to B
+ emitter.emit("B")
+ kairosNetwork.transact {
+ assertEquals(firstStateA.sample(), 2)
+ assertEquals(firstStateB.sample(), 1)
+ }
+
+ // activate a new entry for A, disabling the old entry
+ reset.emit("A")
+ val secondStateA =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A", "B"))
+ stateMap.getValue("A").also {
+ assertEquals(it.sample(), 0)
+ assertEquals(firstStateB.sample(), 1)
+ }
+ }
+
+ // emit to A: the new A state updates, but the old one does not
+ emitter.emit("A")
+ kairosNetwork.transact {
+ assertEquals(firstStateA.sample(), 2)
+ assertEquals(secondStateA.sample(), 1)
+ }
+ }
+ }
+
+ private fun runSample(
+ dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+ block: BuildScope.() -> Unit,
+ ) {
+ runTest(dispatcher, timeout = 1.seconds) {
+ val kairosNetwork = backgroundScope.launchKairosNetwork()
+ backgroundScope.launch { kairosNetwork.activateSpec { block() } }
+ }
+ }
+}
+
+private fun <T> assertEquals(actual: T, expected: T) = Assert.assertEquals(expected, actual)
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
index 150b462..ffe6e95 100644
--- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
@@ -1,14 +1,12 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.maybe
-import com.android.systemui.kairos.util.none
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
@@ -142,7 +140,7 @@
// convert Eventss to States so that they can be combined
val combined =
- left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r ->
+ left.holdState("left" to 0).combine(right.holdState("right" to 0)) { l, r ->
l to r
}
combined.changes // get State changes
@@ -590,7 +588,7 @@
intStopEmitter.emit(Unit) // intAH.complete()
runCurrent()
- // assertEquals(just(10), network.await())
+ // assertEquals(present(10), network.await())
}
@Test
@@ -692,12 +690,12 @@
}
runCurrent()
- emitter.emit(Left(10))
+ emitter.emit(First(10))
runCurrent()
assertEquals(20, result.value)
- emitter.emit(Right(30))
+ emitter.emit(Second(30))
runCurrent()
assertEquals(-30, result.value)
@@ -908,7 +906,7 @@
activateSpecWithResult(network) {
val bA = updater.map { it * 2 }.holdState(0)
val bB = updater.holdState(0)
- val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b }
+ val combineD: State<Pair<Int, Int>> = bA.combine(bB) { a, b -> a to b }
val sampleS = emitter.sample(combineD) { _, b -> b }
sampleS.nextDeferred()
}
@@ -1142,16 +1140,13 @@
val eRemoved =
childChangeById
.eventsForKey(childId)
- .filter { it === None }
+ .filter { it === Absent }
.onEach {
println(
"removing? (groupId=$groupId, childId=$childId)"
)
}
- .nextOnly(
- name =
- "eRemoved(groupId=$groupId, childId=$childId)"
- )
+ .nextOnly()
val addChild: Events<Maybe<State<String>>> =
now.map { mChild }
@@ -1168,13 +1163,9 @@
"removeChild (groupId=$groupId, childId=$childId)"
)
}
- .map { none() }
+ .map { Maybe.absent() }
- addChild.mergeWith(
- removeChild,
- name =
- "childUpdatesMerged(groupId=$groupId, childId=$childId)",
- ) { _, _ ->
+ addChild.mergeWith(removeChild) { _, _ ->
error("unexpected coincidence")
}
}
@@ -1182,7 +1173,7 @@
}
val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> =
map.onEach { println("merge patch: $it") }
- .mergeIncrementallyPromptly(name = "mergeIncrementally")
+ .mergeEventsIncrementallyPromptly()
mergeIncrementally
.onEach { println("foldmap patch: $it") }
.foldStateMapIncrementally()
@@ -1203,14 +1194,14 @@
val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>()
println()
println("init outer 0")
- e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") })))
+ e.emit(mapOf(0 to Maybe.present(emitter2.onEach { println("emitter2 emit: $it") })))
runCurrent()
assertEquals(mapOf(0 to emptyMap()), state.value)
println()
println("init inner 10")
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(0, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(0, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value)
@@ -1218,19 +1209,19 @@
// replace
println()
println("replace inner 10")
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(1, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value)
// remove
- emitter2.emit(mapOf(10 to none()))
+ emitter2.emit(mapOf(10 to Maybe.absent()))
runCurrent()
assertEquals(mapOf(0 to emptyMap()), state.value)
// add again
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(2, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
@@ -1242,9 +1233,9 @@
// batch update
emitter2.emit(
mapOf(
- 10 to none(),
- 11 to just(MutableStateFlow("(0, 11)")),
- 12 to just(MutableStateFlow("(0, 12)")),
+ 10 to Maybe.absent(),
+ 11 to Maybe.present(MutableStateFlow("(0, 11)")),
+ 12 to Maybe.present(MutableStateFlow("(0, 12)")),
)
)
runCurrent()
@@ -1278,7 +1269,7 @@
}
var outerCount = 0
- val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+ val lastEvent: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
flowOfFlows
.map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
.pairwise(MutableStateFlow(null))
@@ -1296,18 +1287,18 @@
assertEquals(1, outerCount)
// assertEquals(1, incCount.subscriptionCount)
- assertNull(laseventss.value.second.value)
+ assertNull(lastEvent.value.second.value)
incCount.emit(Unit)
runCurrent()
println("checking")
- assertEquals(1, laseventss.value.second.value)
+ assertEquals(1, lastEvent.value.second.value)
incCount.emit(Unit)
runCurrent()
- assertEquals(2, laseventss.value.second.value)
+ assertEquals(2, lastEvent.value.second.value)
newCount.emit(newFlow())
runCurrent()
@@ -1315,9 +1306,9 @@
runCurrent()
// verify old flow is not getting updates
- assertEquals(2, laseventss.value.first.value)
+ assertEquals(2, lastEvent.value.first.value)
// but the new one is
- assertEquals(1, laseventss.value.second.value)
+ assertEquals(1, lastEvent.value.second.value)
}
@Test
@@ -1326,7 +1317,7 @@
var observedCount: Int? = null
activateSpec(network) {
val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } }
- deferredBuildScopeAction { c.get().observe { observedCount = it } }
+ deferredBuildScopeAction { c.value.observe { observedCount = it } }
}
runCurrent()
assertEquals(0, observedCount)
@@ -1385,7 +1376,7 @@
activateSpec(network) {
val handle =
input.observe {
- effectCoroutineScope.launch {
+ launch {
runningCount++
awaitClose { runningCount-- }
}
@@ -1420,7 +1411,7 @@
val specJob =
activateSpec(network) {
input.takeUntil(stopper).observe {
- effectCoroutineScope.launch {
+ launch {
runningCount++
awaitClose { runningCount-- }
}
diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp
index ac545df..c4086c5 100644
--- a/ravenwood/tests/bivalenttest/Android.bp
+++ b/ravenwood/tests/bivalenttest/Android.bp
@@ -40,6 +40,7 @@
"junit-params",
"platform-parametric-runner-lib",
+ "platform-compat-test-rules",
// To make sure it won't cause VerifyError (b/324063814)
"platformprotosnano",
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
index 882c91c..540b082 100644
--- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt
@@ -16,31 +16,52 @@
package com.android.ravenwoodtest.bivalenttest.compat
import android.app.compat.CompatChanges
+import android.compat.testing.PlatformCompatChangeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.internal.ravenwood.RavenwoodEnvironment.CompatIdsForTest
-import org.junit.Assert
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RavenwoodCompatFrameworkTest {
+
+ @get:Rule
+ val compatRule = PlatformCompatChangeRule()
+
@Test
fun testEnabled() {
- Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1))
+ assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1))
}
@Test
fun testDisabled() {
- Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_2))
+ assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_2))
}
@Test
fun testEnabledAfterSForUApps() {
- Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_3))
+ assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_3))
}
@Test
fun testEnabledAfterUForUApps() {
- Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4))
+ assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4))
+ }
+
+ @Test
+ @EnableCompatChanges(CompatIdsForTest.TEST_COMPAT_ID_5)
+ fun testEnableCompatChanges() {
+ assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_5))
+ }
+
+ @Test
+ @DisableCompatChanges(CompatIdsForTest.TEST_COMPAT_ID_5)
+ fun testDisableCompatChanges() {
+ assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_5))
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
index a69ecec..e3d7062 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickController.java
@@ -18,6 +18,8 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static com.android.server.accessibility.AutoclickIndicatorView.SHOW_INDICATOR_DELAY_TIME;
+
import android.accessibilityservice.AccessibilityTrace;
import android.annotation.NonNull;
import android.content.ContentResolver;
@@ -286,8 +288,6 @@
}
public void update() {
- // TODO(b/383901288): update delay time once determined by UX.
- long SHOW_INDICATOR_DELAY_TIME = 150;
long scheduledShowIndicatorTime =
SystemClock.uptimeMillis() + SHOW_INDICATOR_DELAY_TIME;
// If there already is a scheduled show indicator at time before the updated time, just
@@ -432,6 +432,10 @@
*/
public void updateDelay(int delay) {
mDelay = delay;
+
+ if (Flags.enableAutoclickIndicator() && mAutoclickIndicatorView != null) {
+ mAutoclickIndicatorView.setAnimationDuration(delay - SHOW_INDICATOR_DELAY_TIME);
+ }
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
index 54c31e5..816d8e45 100644
--- a/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
+++ b/services/accessibility/java/com/android/server/accessibility/AutoclickIndicatorView.java
@@ -16,25 +16,43 @@
package com.android.server.accessibility;
+import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
+import android.graphics.RectF;
import android.util.DisplayMetrics;
import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.LinearInterpolator;
// A visual indicator for the autoclick feature.
public class AutoclickIndicatorView extends View {
private static final String TAG = AutoclickIndicatorView.class.getSimpleName();
+ // TODO(b/383901288): update delay time once determined by UX.
+ static final int SHOW_INDICATOR_DELAY_TIME = 150;
+
+ static final int MINIMAL_ANIMATION_DURATION = 50;
+
// TODO(b/383901288): allow users to customize the indicator area.
static final float RADIUS = 50;
private final Paint mPaint;
+ private final ValueAnimator mAnimator;
+
+ private final RectF mRingRect;
+
// x and y coordinates of the visual indicator.
private float mX;
private float mY;
+ // Current sweep angle of the animated ring.
+ private float mSweepAngle;
+
+ private int mAnimationDuration = AccessibilityManager.AUTOCLICK_DELAY_DEFAULT;
+
// Status of whether the visual indicator should display or not.
private boolean showIndicator = false;
@@ -46,6 +64,18 @@
mPaint.setARGB(255, 52, 103, 235);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
+
+ mAnimator = ValueAnimator.ofFloat(0, 360);
+ mAnimator.setDuration(mAnimationDuration);
+ mAnimator.setInterpolator(new LinearInterpolator());
+ mAnimator.addUpdateListener(
+ animation -> {
+ mSweepAngle = (float) animation.getAnimatedValue();
+ // Redraw the view with the updated angle.
+ invalidate();
+ });
+
+ mRingRect = new RectF();
}
@Override
@@ -53,7 +83,12 @@
super.onDraw(canvas);
if (showIndicator) {
- canvas.drawCircle(mX, mY, RADIUS, mPaint);
+ mRingRect.set(
+ /* left= */ mX - RADIUS,
+ /* top= */ mY - RADIUS,
+ /* right= */ mX + RADIUS,
+ /* bottom= */ mY + RADIUS);
+ canvas.drawArc(mRingRect, /* startAngle= */ -90, mSweepAngle, false, mPaint);
}
}
@@ -75,10 +110,17 @@
public void redrawIndicator() {
showIndicator = true;
invalidate();
+ mAnimator.start();
}
public void clearIndicator() {
showIndicator = false;
+ mAnimator.cancel();
invalidate();
}
+
+ public void setAnimationDuration(int duration) {
+ mAnimationDuration = Math.max(duration, MINIMAL_ANIMATION_DURATION);
+ mAnimator.setDuration(mAnimationDuration);
+ }
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 1f3b316..aeb2f5e 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -176,7 +176,7 @@
public VirtualDeviceManagerService(Context context) {
super(context);
mImpl = new VirtualDeviceManagerImpl();
- mNativeImpl = Flags.enableNativeVdm() ? new VirtualDeviceManagerNativeImpl() : null;
+ mNativeImpl = new VirtualDeviceManagerNativeImpl();
mLocalService = new LocalService();
}
@@ -208,9 +208,7 @@
@RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
public void onStart() {
publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl);
- if (Flags.enableNativeVdm()) {
- publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl);
- }
+ publishBinderService(VIRTUAL_DEVICE_NATIVE_SERVICE, mNativeImpl);
publishLocalService(VirtualDeviceManagerInternal.class, mLocalService);
ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService(
ActivityTaskManagerInternal.class);
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 41b4cbd..6cca7d1 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -8950,18 +8950,12 @@
if (!mAm.mConstants.mFgsStartRestrictionCheckCallerTargetSdk) {
return true; // In this case, we only check the service's target SDK level.
}
- final int callingUid;
- if (Flags.newFgsRestrictionLogic()) {
- // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
- if (actualCallingUid == Process.SYSTEM_UID) {
- return true;
- }
- callingUid = actualCallingUid;
- } else {
- // Legacy logic used mRecentCallingUid.
- callingUid = r.mRecentCallingUid;
+ // We always consider SYSTEM_UID to target S+, so just enable the restrictions.
+ if (actualCallingUid == Process.SYSTEM_UID) {
+ return true;
}
- if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID, callingUid)) {
+ if (!CompatChanges.isChangeEnabled(FGS_BG_START_RESTRICTION_CHANGE_ID,
+ actualCallingUid)) {
return false; // If the caller targets < S, then we still disable the restrictions.
}
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 92d33c9..ca34a13 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -278,24 +278,21 @@
* Whether to use the new "while-in-use permission" logic for FGS start
*/
private boolean useNewWiuLogic_forStart() {
- return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
- && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
+ return CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_START, appInfo.uid);
}
/**
* Whether to use the new "while-in-use permission" logic for capabilities
*/
private boolean useNewWiuLogic_forCapabilities() {
- return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
- && CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
+ return CompatChanges.isChangeEnabled(USE_NEW_WIU_LOGIC_FOR_CAPABILITIES, appInfo.uid);
}
/**
* Whether to use the new "FGS BG start exemption" logic.
*/
private boolean useNewBfslLogic() {
- return Flags.newFgsRestrictionLogic() // This flag should only be set on V+
- && CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
+ return CompatChanges.isChangeEnabled(USE_NEW_BFSL_LOGIC, appInfo.uid);
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c31b9ef..ec74f60 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -160,6 +160,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -1920,8 +1921,14 @@
return false;
}
- mHandler.post(() -> startUserInternalOnHandler(userId, oldUserId, userStartMode,
- unlockListener, callingUid, callingPid));
+ final Runnable continueStartUserInternal = () -> continueStartUserInternal(userInfo,
+ oldUserId, userStartMode, unlockListener, callingUid, callingPid);
+ if (foreground) {
+ mHandler.post(() -> dispatchOnBeforeUserSwitching(userId, () ->
+ mHandler.post(continueStartUserInternal)));
+ } else {
+ continueStartUserInternal.run();
+ }
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1929,11 +1936,11 @@
return true;
}
- private void startUserInternalOnHandler(int userId, int oldUserId, int userStartMode,
+ private void continueStartUserInternal(UserInfo userInfo, int oldUserId, int userStartMode,
IProgressListener unlockListener, int callingUid, int callingPid) {
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
final boolean foreground = userStartMode == USER_START_MODE_FOREGROUND;
- final UserInfo userInfo = getUserInfo(userId);
+ final int userId = userInfo.id;
boolean needStart = false;
boolean updateUmState = false;
@@ -1995,7 +2002,6 @@
// it should be moved outside, but for now it's not as there are many calls to
// external components here afterwards
updateProfileRelatedCaches();
- dispatchOnBeforeUserSwitching(userId);
mInjector.getWindowManager().setCurrentUser(userId);
mInjector.reportCurWakefulnessUsageEvent();
// Once the internal notion of the active user has switched, we lock the device
@@ -2296,25 +2302,42 @@
mUserSwitchObservers.finishBroadcast();
}
- private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId) {
+ private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId, Runnable onComplete) {
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("dispatchOnBeforeUserSwitching-" + newUserId);
- final int observerCount = mUserSwitchObservers.beginBroadcast();
- for (int i = 0; i < observerCount; i++) {
- final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
- t.traceBegin("onBeforeUserSwitching-" + name);
+ final AtomicBoolean isFirst = new AtomicBoolean(true);
+ startTimeoutForOnBeforeUserSwitching(isFirst, onComplete);
+ informUserSwitchObservers((observer, callback) -> {
try {
- mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId);
+ observer.onBeforeUserSwitching(newUserId, callback);
} catch (RemoteException e) {
- // Ignore
- } finally {
- t.traceEnd();
+ // ignore
}
- }
- mUserSwitchObservers.finishBroadcast();
+ }, () -> {
+ if (isFirst.getAndSet(false)) {
+ onComplete.run();
+ }
+ }, "onBeforeUserSwitching");
t.traceEnd();
}
+ private void startTimeoutForOnBeforeUserSwitching(AtomicBoolean isFirst,
+ Runnable onComplete) {
+ final long timeout = getUserSwitchTimeoutMs();
+ mHandler.postDelayed(() -> {
+ if (isFirst.getAndSet(false)) {
+ String unresponsiveObservers;
+ synchronized (mLock) {
+ unresponsiveObservers = String.join(", ", mCurWaitingUserSwitchCallbacks);
+ }
+ Slogf.e(TAG, "Timeout on dispatchOnBeforeUserSwitching. These UserSwitchObservers "
+ + "did not respond in " + timeout + "ms: " + unresponsiveObservers + ".");
+ onComplete.run();
+ }
+ }, timeout);
+ }
+
+
/** Called on handler thread */
@VisibleForTesting
void dispatchUserSwitchComplete(@UserIdInt int oldUserId, @UserIdInt int newUserId) {
@@ -2527,70 +2550,76 @@
t.traceBegin("dispatchUserSwitch-" + oldUserId + "-to-" + newUserId);
EventLog.writeEvent(EventLogTags.UC_DISPATCH_USER_SWITCH, oldUserId, newUserId);
-
- final int observerCount = mUserSwitchObservers.beginBroadcast();
- if (observerCount > 0) {
- final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
- synchronized (mLock) {
- uss.switching = true;
- mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks;
+ uss.switching = true;
+ informUserSwitchObservers((observer, callback) -> {
+ try {
+ observer.onUserSwitching(newUserId, callback);
+ } catch (RemoteException e) {
+ // ignore
}
- final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount);
- final long userSwitchTimeoutMs = getUserSwitchTimeoutMs();
- final long dispatchStartedTime = SystemClock.elapsedRealtime();
- for (int i = 0; i < observerCount; i++) {
- final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime();
- try {
- // Prepend with unique prefix to guarantee that keys are unique
- final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
- synchronized (mLock) {
- curWaitingUserSwitchCallbacks.add(name);
- }
- final IRemoteCallback callback = new IRemoteCallback.Stub() {
- @Override
- public void sendResult(Bundle data) throws RemoteException {
- asyncTraceEnd("onUserSwitching-" + name, newUserId);
- synchronized (mLock) {
- long delayForObserver = SystemClock.elapsedRealtime()
- - dispatchStartedTimeForObserver;
- if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) {
- Slogf.w(TAG, "User switch slowed down by observer " + name
- + ": result took " + delayForObserver
- + " ms to process.");
- }
-
- long totalDelay = SystemClock.elapsedRealtime()
- - dispatchStartedTime;
- if (totalDelay > userSwitchTimeoutMs) {
- Slogf.e(TAG, "User switch timeout: observer " + name
- + "'s result was received " + totalDelay
- + " ms after dispatchUserSwitch.");
- }
-
- curWaitingUserSwitchCallbacks.remove(name);
- // Continue switching if all callbacks have been notified and
- // user switching session is still valid
- if (waitingCallbacksCount.decrementAndGet() == 0
- && (curWaitingUserSwitchCallbacks
- == mCurWaitingUserSwitchCallbacks)) {
- sendContinueUserSwitchLU(uss, oldUserId, newUserId);
- }
- }
- }
- };
- asyncTraceBegin("onUserSwitching-" + name, newUserId);
- mUserSwitchObservers.getBroadcastItem(i).onUserSwitching(newUserId, callback);
- } catch (RemoteException e) {
- // Ignore
- }
- }
- } else {
+ }, () -> {
synchronized (mLock) {
sendContinueUserSwitchLU(uss, oldUserId, newUserId);
}
+ }, "onUserSwitching");
+ t.traceEnd();
+ }
+
+ void informUserSwitchObservers(BiConsumer<IUserSwitchObserver, IRemoteCallback> consumer,
+ final Runnable onComplete, String trace) {
+ final int observerCount = mUserSwitchObservers.beginBroadcast();
+ if (observerCount == 0) {
+ onComplete.run();
+ mUserSwitchObservers.finishBroadcast();
+ return;
+ }
+ final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>();
+ synchronized (mLock) {
+ mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks;
+ }
+ final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount);
+ final long userSwitchTimeoutMs = getUserSwitchTimeoutMs();
+ final long dispatchStartedTime = SystemClock.elapsedRealtime();
+ for (int i = 0; i < observerCount; i++) {
+ final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime();
+ // Prepend with unique prefix to guarantee that keys are unique
+ final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i);
+ synchronized (mLock) {
+ curWaitingUserSwitchCallbacks.add(name);
+ }
+ final IRemoteCallback callback = new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ asyncTraceEnd(trace + "-" + name, 0);
+ synchronized (mLock) {
+ long delayForObserver = SystemClock.elapsedRealtime()
+ - dispatchStartedTimeForObserver;
+ if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) {
+ Slogf.w(TAG, "User switch slowed down by observer " + name
+ + ": result took " + delayForObserver
+ + " ms to process. " + trace);
+ }
+ long totalDelay = SystemClock.elapsedRealtime() - dispatchStartedTime;
+ if (totalDelay > userSwitchTimeoutMs) {
+ Slogf.e(TAG, "User switch timeout: observer " + name
+ + "'s result was received " + totalDelay
+ + " ms after dispatchUserSwitch. " + trace);
+ }
+ curWaitingUserSwitchCallbacks.remove(name);
+ // Continue switching if all callbacks have been notified and
+ // user switching session is still valid
+ if (waitingCallbacksCount.decrementAndGet() == 0
+ && (curWaitingUserSwitchCallbacks
+ == mCurWaitingUserSwitchCallbacks)) {
+ onComplete.run();
+ }
+ }
+ }
+ };
+ asyncTraceBegin(trace + "-" + name, 0);
+ consumer.accept(mUserSwitchObservers.getBroadcastItem(i), callback);
}
mUserSwitchObservers.finishBroadcast();
- t.traceEnd(); // end dispatchUserSwitch-
}
@GuardedBy("mLock")
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0fd4716..bd27142 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -242,6 +242,7 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.SystemPropertySetter;
import android.view.Display;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityManager;
@@ -11032,7 +11033,7 @@
@GuardedBy("mLock")
private void updateLocked() {
String n = Long.toString(mToken++);
- SystemProperties.set(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n);
+ SystemPropertySetter.setWithRetry(PermissionManager.CACHE_KEY_PACKAGE_INFO_NOTIFY, n);
}
private void trigger() {
diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java
index e89f43b..20c3327 100644
--- a/services/core/java/com/android/server/compat/CompatConfig.java
+++ b/services/core/java/com/android/server/compat/CompatConfig.java
@@ -876,7 +876,28 @@
}
@Nullable
+ @android.ravenwood.annotation.RavenwoodReplace(
+ blockedBy = PackageManager.class,
+ reason = "PackageManager.getApplicationInfo() isn't supported yet")
private Long getVersionCodeOrNull(String packageName) {
+ return getVersionCodeOrNullImpl(packageName);
+ }
+
+ @SuppressWarnings("unused")
+ @Nullable
+ private Long getVersionCodeOrNull$ravenwood(String packageName) {
+ try {
+ // It's possible that the context is mocked, try the real method first
+ return getVersionCodeOrNullImpl(packageName);
+ } catch (Throwable e) {
+ // For now, Ravenwood doesn't support the concept of "app updates", so let's
+ // just use a fixed version code for all packages.
+ return 1L;
+ }
+ }
+
+ @Nullable
+ private Long getVersionCodeOrNullImpl(String packageName) {
try {
ApplicationInfo applicationInfo = mContext.getPackageManager().getApplicationInfo(
packageName, MATCH_ANY_USER);
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index c384b54..9349ea5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1030,10 +1030,20 @@
}
@ServiceThreadOnly
+ void addAndStartAction(final HdmiCecFeatureAction action, final boolean remove) {
+ assertRunOnServiceThread();
+ if (hasAction(action.getClass()) && remove) {
+ // If the action is currently running, remove it and restart it.
+ Slog.i(TAG, action.getClass().getName() + " is in progress. Restarting.");
+ removeAction(action.getClass());
+ }
+ addAndStartAction(action);
+ }
+
+ @ServiceThreadOnly
void startNewAvbAudioStatusAction(int targetAddress) {
assertRunOnServiceThread();
- removeAction(AbsoluteVolumeAudioStatusAction.class);
- addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
+ addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress), true);
}
@ServiceThreadOnly
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
index 1e90ab2..510e4f5 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java
@@ -317,11 +317,7 @@
if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
|| ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON)
&& lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) {
- if (hasAction(SystemAudioInitiationActionFromAvr.class)) {
- Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting.");
- removeAction(SystemAudioInitiationActionFromAvr.class);
- }
- addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+ addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
}
}
@@ -457,6 +453,7 @@
HdmiLogger.debug("AVR device is not directly connected with TV");
return Constants.ABORT_NOT_IN_CORRECT_MODE;
} else {
+ // Action has been removed if it existed, do not attempt to remove again before start.
addAndStartAction(new ArcInitiationActionFromAvr(this));
return Constants.HANDLED;
}
@@ -477,11 +474,9 @@
&& !getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.isEmpty()) {
IHdmiControlCallback callback =
getActions(ArcTerminationActionFromAvr.class).get(0).mCallbacks.get(0);
- removeAction(ArcTerminationActionFromAvr.class);
- addAndStartAction(new ArcTerminationActionFromAvr(this, callback));
+ addAndStartAction(new ArcTerminationActionFromAvr(this, callback), true);
} else {
- removeAction(ArcTerminationActionFromAvr.class);
- addAndStartAction(new ArcTerminationActionFromAvr(this));
+ addAndStartAction(new ArcTerminationActionFromAvr(this), true);
}
return Constants.HANDLED;
}
@@ -1036,11 +1031,7 @@
void onSystemAudioControlFeatureSupportChanged(boolean enabled) {
setSystemAudioControlFeatureEnabled(enabled);
if (enabled) {
- if (hasAction(SystemAudioInitiationActionFromAvr.class)) {
- Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting.");
- removeAction(SystemAudioInitiationActionFromAvr.class);
- }
- addAndStartAction(new SystemAudioInitiationActionFromAvr(this));
+ addAndStartAction(new SystemAudioInitiationActionFromAvr(this), true);
}
}
@@ -1221,8 +1212,7 @@
removeAction(ArcTerminationActionFromAvr.class);
if (SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
&& isDirectConnectToTv() && !isArcEnabled()) {
- removeAction(ArcInitiationActionFromAvr.class);
- addAndStartAction(new ArcInitiationActionFromAvr(this));
+ addAndStartAction(new ArcInitiationActionFromAvr(this), true);
}
}
@@ -1367,10 +1357,6 @@
if (mService.isDeviceDiscoveryHandledByPlayback()) {
return;
}
- if (hasAction(DeviceDiscoveryAction.class)) {
- Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
- removeAction(DeviceDiscoveryAction.class);
- }
DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
new DeviceDiscoveryCallback() {
@Override
@@ -1380,7 +1366,7 @@
}
}
});
- addAndStartAction(action);
+ addAndStartAction(action, true);
}
@Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
index 0b667fc..86abbc4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@@ -21,7 +21,6 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.hardware.display.DeviceProductInfo;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.IHdmiControlCallback;
@@ -32,7 +31,6 @@
import android.os.SystemProperties;
import android.sysprop.HdmiProperties;
import android.util.Slog;
-import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.LocalePicker;
@@ -151,10 +149,6 @@
private void launchDeviceDiscovery() {
assertRunOnServiceThread();
clearDeviceInfoList();
- if (hasAction(DeviceDiscoveryAction.class)) {
- Slog.i(TAG, "Device Discovery Action is in progress. Restarting.");
- removeAction(DeviceDiscoveryAction.class);
- }
DeviceDiscoveryAction action = new DeviceDiscoveryAction(this,
new DeviceDiscoveryAction.DeviceDiscoveryCallback() {
@Override
@@ -163,25 +157,21 @@
mService.getHdmiCecNetwork().addCecDevice(info);
}
- // Since we removed all devices when it starts and device discovery action
- // does not poll local devices, we should put device info of local device
- // manually here.
+ // Since we removed all devices when it starts and device discovery
+ // action does not poll local devices, we should put device info of
+ // local device manually here.
for (HdmiCecLocalDevice device : mService.getAllCecLocalDevices()) {
mService.getHdmiCecNetwork().addCecDevice(device.getDeviceInfo());
}
- List<HotplugDetectionAction> hotplugActions =
- getActions(HotplugDetectionAction.class);
- if (hotplugActions.isEmpty()) {
+ if (!hasAction(HotplugDetectionAction.class)) {
addAndStartAction(
- new HotplugDetectionAction(HdmiCecLocalDevicePlayback.this));
+ new HotplugDetectionAction(
+ HdmiCecLocalDevicePlayback.this));
}
if (mService.isHdmiControlEnhancedBehaviorFlagEnabled()) {
- List<PowerStatusMonitorActionFromPlayback>
- powerStatusMonitorActionsFromPlayback =
- getActions(PowerStatusMonitorActionFromPlayback.class);
- if (powerStatusMonitorActionsFromPlayback.isEmpty()) {
+ if (!hasAction(PowerStatusMonitorActionFromPlayback.class)) {
addAndStartAction(
new PowerStatusMonitorActionFromPlayback(
HdmiCecLocalDevicePlayback.this));
@@ -189,7 +179,7 @@
}
}
});
- addAndStartAction(action);
+ addAndStartAction(action, true);
}
@Override
@@ -235,8 +225,16 @@
invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
return;
}
- removeAction(DeviceSelectActionFromPlayback.class);
- addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback));
+ List<DeviceSelectActionFromPlayback> actions = getActions(
+ DeviceSelectActionFromPlayback.class);
+ if (!actions.isEmpty()) {
+ DeviceSelectActionFromPlayback action = actions.get(0);
+ if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
+ return;
+ }
+ }
+ addAndStartAction(new DeviceSelectActionFromPlayback(this, targetDevice, callback),
+ true);
}
@Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 424102c..3d6d34b 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -220,10 +220,6 @@
List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer
.getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE);
if (bufferedActiveSource.isEmpty()) {
- if (hasAction(RequestActiveSourceAction.class)) {
- Slog.i(TAG, "RequestActiveSourceAction is in progress. Restarting.");
- removeAction(RequestActiveSourceAction.class);
- }
addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
@Override
public void onComplete(int result) {
@@ -231,7 +227,7 @@
launchRoutingControl(routingForBootup);
}
}
- }));
+ }), true);
} else {
addCecDeviceForBufferedActiveSource(bufferedActiveSource.get(0));
}
@@ -328,8 +324,15 @@
invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
return;
}
- removeAction(DeviceSelectActionFromTv.class);
- addAndStartAction(new DeviceSelectActionFromTv(this, targetDevice, callback));
+ List<DeviceSelectActionFromTv> actions = getActions(DeviceSelectActionFromTv.class);
+ if (!actions.isEmpty()) {
+ DeviceSelectActionFromTv action = actions.get(0);
+ if (action.getTargetAddress() == targetDevice.getLogicalAddress()) {
+ return;
+ }
+ }
+ addAndStartAction(new DeviceSelectActionFromTv(this, targetDevice, callback),
+ true);
}
@ServiceThreadOnly
@@ -475,9 +478,8 @@
HdmiCecMessageBuilder.buildRoutingChange(
getDeviceInfo().getLogicalAddress(), oldPath, newPath);
mService.sendCecCommand(routingChange);
- removeAction(RoutingControlAction.class);
addAndStartAction(
- new RoutingControlAction(this, newPath, callback));
+ new RoutingControlAction(this, newPath, callback), true);
}
@ServiceThreadOnly
@@ -801,16 +803,12 @@
mSelectRequestBuffer.process();
resetSelectRequestBuffer();
- List<HotplugDetectionAction> hotplugActions
- = getActions(HotplugDetectionAction.class);
- if (hotplugActions.isEmpty()) {
+ if (!hasAction(HotplugDetectionAction.class)) {
addAndStartAction(
new HotplugDetectionAction(HdmiCecLocalDeviceTv.this));
}
- List<PowerStatusMonitorAction> powerStatusActions
- = getActions(PowerStatusMonitorAction.class);
- if (powerStatusActions.isEmpty()) {
+ if (!hasAction(PowerStatusMonitorAction.class)) {
addAndStartAction(
new PowerStatusMonitorAction(HdmiCecLocalDeviceTv.this));
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 35ef18b..bd8b67b9 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -17,7 +17,6 @@
package com.android.server.hdmi;
import static android.media.tv.flags.Flags.hdmiControlEnhancedBehavior;
-
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_ADD_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.DEVICE_EVENT_REMOVE_DEVICE;
import static android.hardware.hdmi.HdmiControlManager.EARC_FEATURE_DISABLED;
@@ -107,7 +106,6 @@
import android.util.SparseArray;
import android.view.Display;
import android.view.KeyEvent;
-import android.view.WindowManager;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -1218,9 +1216,6 @@
audioSystem.terminateSystemAudioMode();
}
if (isArcEnabled) {
- if (audioSystem.hasAction(ArcTerminationActionFromAvr.class)) {
- audioSystem.removeAction(ArcTerminationActionFromAvr.class);
- }
audioSystem.addAndStartAction(new ArcTerminationActionFromAvr(audioSystem,
new IHdmiControlCallback.Stub() {
@Override
@@ -1228,7 +1223,7 @@
mAddressAllocated = false;
initializeCecLocalDevices(INITIATED_BY_SOUNDBAR_MODE);
}
- }));
+ }), true);
}
}
if (!isArcEnabled) {
diff --git a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
index 9a3cde1..d05ded5 100644
--- a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
+++ b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java
@@ -68,6 +68,8 @@
private boolean handleReportPowerStatusFromTv(HdmiCecMessage cmd) {
int powerStatus = cmd.getParams()[0] & 0xFF;
+ mState = STATE_WAIT_FOR_NEXT_MONITORING;
+ addTimer(mState, MONITORING_INTERVAL_MS);
if (powerStatus == POWER_STATUS_STANDBY) {
Slog.d(TAG, "TV reported it turned off, going to sleep.");
source().getService().standby();
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
index 7698a87..740c4f1 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointManager.java
@@ -36,6 +36,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
/**
* A class that manages registration/unregistration of clients and manages messages to/from clients.
@@ -312,15 +313,9 @@
@Override
public void onCloseEndpointSession(int sessionId, byte reason) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onCloseEndpointSession(sessionId, reason);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId, (broker) -> broker.onCloseEndpointSession(sessionId, reason));
if (!callbackInvoked) {
Log.w(TAG, "onCloseEndpointSession: unknown session ID " + sessionId);
}
@@ -328,15 +323,9 @@
@Override
public void onEndpointSessionOpenComplete(int sessionId) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onEndpointSessionOpenComplete(sessionId);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId, (broker) -> broker.onEndpointSessionOpenComplete(sessionId));
if (!callbackInvoked) {
Log.w(TAG, "onEndpointSessionOpenComplete: unknown session ID " + sessionId);
}
@@ -344,15 +333,9 @@
@Override
public void onMessageReceived(int sessionId, HubMessage message) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onMessageReceived(sessionId, message);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId, (broker) -> broker.onMessageReceived(sessionId, message));
if (!callbackInvoked) {
Log.w(TAG, "onMessageReceived: unknown session ID " + sessionId);
}
@@ -360,20 +343,38 @@
@Override
public void onMessageDeliveryStatusReceived(int sessionId, int sequenceNumber, byte errorCode) {
- boolean callbackInvoked = false;
- for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
- if (broker.hasSessionId(sessionId)) {
- broker.onMessageDeliveryStatusReceived(sessionId, sequenceNumber, errorCode);
- callbackInvoked = true;
- break;
- }
- }
-
+ boolean callbackInvoked =
+ invokeCallbackForMatchingSession(
+ sessionId,
+ (broker) ->
+ broker.onMessageDeliveryStatusReceived(
+ sessionId, sequenceNumber, errorCode));
if (!callbackInvoked) {
Log.w(TAG, "onMessageDeliveryStatusReceived: unknown session ID " + sessionId);
}
}
+ /**
+ * Invokes a callback for a session with matching ID.
+ *
+ * @param callback The callback to execute
+ * @return true if a callback was executed
+ */
+ private boolean invokeCallbackForMatchingSession(
+ int sessionId, Consumer<ContextHubEndpointBroker> callback) {
+ for (ContextHubEndpointBroker broker : mEndpointMap.values()) {
+ if (broker.hasSessionId(sessionId)) {
+ try {
+ callback.accept(broker);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Exception while invoking callback", e);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Unregister the hub (called during init() failure). Silence errors. */
private void unregisterHub() {
try {
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 54ed5a9..2615a76 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -756,9 +756,7 @@
@Override
public List<HubInfo> getHubs() throws RemoteException {
super.getHubs_enforcePermission();
- if (mHubInfoRegistry == null) {
- return Collections.emptyList();
- }
+ checkHubDiscoveryPreconditions();
return mHubInfoRegistry.getHubs();
}
@@ -766,9 +764,7 @@
@Override
public List<HubEndpointInfo> findEndpoints(long endpointId) {
super.findEndpoints_enforcePermission();
- if (mHubInfoRegistry == null) {
- return Collections.emptyList();
- }
+ checkEndpointDiscoveryPreconditions();
return mHubInfoRegistry.findEndpoints(endpointId);
}
@@ -776,9 +772,7 @@
@Override
public List<HubEndpointInfo> findEndpointsWithService(String serviceDescriptor) {
super.findEndpointsWithService_enforcePermission();
- if (mHubInfoRegistry == null) {
- return Collections.emptyList();
- }
+ checkEndpointDiscoveryPreconditions();
return mHubInfoRegistry.findEndpointsWithService(serviceDescriptor);
}
@@ -834,6 +828,13 @@
}
}
+ private void checkHubDiscoveryPreconditions() {
+ if (mHubInfoRegistry == null) {
+ Log.e(TAG, "Hub registry failed to initialize");
+ throw new UnsupportedOperationException("Hub discovery is not supported");
+ }
+ }
+
/**
* Creates an internal load transaction callback to be used for old API clients
*
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 3f91575..286238e 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -451,6 +451,7 @@
* @param profileUserId profile user Id
* @param profileUserPassword profile original password (when it has separated lock).
*/
+ @GuardedBy("mSpManager")
private void tieProfileLockIfNecessary(int profileUserId,
LockscreenCredential profileUserPassword) {
// Only for profiles that shares credential with parent
@@ -909,14 +910,8 @@
// Hide notification first, as tie profile lock takes time
hideEncryptionNotification(new UserHandle(userId));
- if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
- synchronized (mSpManager) {
- tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
- }
- } else {
- if (isCredentialSharableWithParent(userId)) {
- tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
- }
+ synchronized (mSpManager) {
+ tieProfileLockIfNecessary(userId, LockscreenCredential.createNone());
}
}
});
@@ -1380,11 +1375,7 @@
mStorage.removeChildProfileLock(userId);
removeKeystoreProfileKey(userId);
} else {
- if (android.app.admin.flags.Flags.fixRaceConditionInTieProfileLock()) {
- synchronized (mSpManager) {
- tieProfileLockIfNecessary(userId, profileUserPassword);
- }
- } else {
+ synchronized (mSpManager) {
tieProfileLockIfNecessary(userId, profileUserPassword);
}
}
diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
index 38f3939..cc5c88b 100644
--- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java
+++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java
@@ -50,6 +50,10 @@
*/
static Pair<String, ActorState> getPackageNameForActor(@NonNull String actorUriString,
@NonNull Map<String, Map<String, String>> namedActors) {
+ if (namedActors.isEmpty()) {
+ return Pair.create(null, ActorState.NO_NAMED_ACTORS);
+ }
+
Uri actorUri = Uri.parse(actorUriString);
String actorScheme = actorUri.getScheme();
@@ -58,10 +62,6 @@
return Pair.create(null, ActorState.INVALID_OVERLAYABLE_ACTOR_NAME);
}
- if (namedActors.isEmpty()) {
- return Pair.create(null, ActorState.NO_NAMED_ACTORS);
- }
-
String actorNamespace = actorUri.getAuthority();
Map<String, String> namespace = namedActors.get(actorNamespace);
if (ArrayUtils.isEmpty(namespace)) {
diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java
index 1726f0d..a6f2a37 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -121,6 +121,8 @@
@VisibleForTesting final long mHintSessionPreferredRate;
@VisibleForTesting static final int MAX_GRAPHICS_PIPELINE_THREADS_COUNT = 5;
+ private static final int DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT = 5;
+ private static final int DEFAULT_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS = 50;
// Multi-level map storing all active AppHintSessions.
// First level is keyed by the UID of the client process creating the session.
@@ -206,12 +208,17 @@
"persist.hms.check_headroom_affinity";
private static final String PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS =
"persist.hms.check_headroom_proc_stat_min_millis";
+ private static final String PROPERTY_CPU_HEADROOM_TID_MAX_CNT =
+ "persist.hms.cpu_headroom_tid_max_cnt";
private Boolean mFMQUsesIntegratedEventFlag = false;
private final Object mCpuHeadroomLock = new Object();
@VisibleForTesting
final float mJiffyMillis;
+ private final boolean mCheckHeadroomTid;
+ private final boolean mCheckHeadroomAffinity;
private final int mCheckHeadroomProcStatMinMillis;
+ private final int mCpuHeadroomMaxTidCnt;
@GuardedBy("mCpuHeadroomLock")
private long mLastCpuUserModeTimeCheckedMillis = 0;
@GuardedBy("mCpuHeadroomLock")
@@ -339,13 +346,23 @@
mUidToLastUserModeJiffies = new ArrayMap<>();
long jiffyHz = Os.sysconf(OsConstants._SC_CLK_TCK);
mJiffyMillis = 1000.0f / jiffyHz;
+ mCheckHeadroomTid = SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_TID, true);
+ mCheckHeadroomAffinity = SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_AFFINITY,
+ true);
mCheckHeadroomProcStatMinMillis = SystemProperties.getInt(
- PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS, 50);
+ PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS,
+ DEFAULT_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS);
+ mCpuHeadroomMaxTidCnt = Math.min(SystemProperties.getInt(
+ PROPERTY_CPU_HEADROOM_TID_MAX_CNT, DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT),
+ mSupportInfo.headroom.cpuMaxTidCount);
} else {
mCpuHeadroomCache = null;
mUidToLastUserModeJiffies = null;
mJiffyMillis = 0.0f;
+ mCheckHeadroomTid = true;
+ mCheckHeadroomAffinity = true;
mCheckHeadroomProcStatMinMillis = 0;
+ mCpuHeadroomMaxTidCnt = 0;
}
if (mSupportInfo.headroom.isGpuSupported) {
mGpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.gpuMinIntervalMillis);
@@ -1577,8 +1594,7 @@
if (params.usesDeviceHeadroom) {
halParams.tids = new int[]{};
} else if (params.tids != null && params.tids.length > 0) {
- if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && SystemProperties.getBoolean(
- PROPERTY_CHECK_HEADROOM_TID, true)) {
+ if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && mCheckHeadroomTid) {
final int tgid = Process.getThreadGroupLeader(Binder.getCallingPid());
for (int tid : params.tids) {
if (Process.getThreadGroupLeader(tid) != tgid) {
@@ -1588,8 +1604,8 @@
}
}
}
- if (cpuHeadroomAffinityCheck() && params.tids.length > 1
- && SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_AFFINITY, true)) {
+ if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity
+ && params.tids.length > 1) {
checkThreadAffinityForTids(params.tids);
}
halParams.tids = params.tids;
@@ -1709,15 +1725,22 @@
throw new IllegalArgumentException(
"Unknown CPU headroom calculation type " + (int) params.calculationType);
}
- if (params.calculationWindowMillis < 50 || params.calculationWindowMillis > 10000) {
+ if (params.calculationWindowMillis < mSupportInfo.headroom.cpuMinCalculationWindowMillis
+ || params.calculationWindowMillis
+ > mSupportInfo.headroom.cpuMaxCalculationWindowMillis) {
throw new IllegalArgumentException(
- "Invalid CPU headroom calculation window, expected [50, 10000] but got "
+ "Invalid CPU headroom calculation window, expected ["
+ + mSupportInfo.headroom.cpuMinCalculationWindowMillis
+ + ", "
+ + mSupportInfo.headroom.cpuMaxCalculationWindowMillis
+ + "] but got "
+ params.calculationWindowMillis);
}
if (!params.usesDeviceHeadroom) {
- if (params.tids != null && params.tids.length > 5) {
+ if (params.tids != null && params.tids.length > mCpuHeadroomMaxTidCnt) {
throw new IllegalArgumentException(
- "More than 5 TIDs requested: " + params.tids.length);
+ "More than " + mCpuHeadroomMaxTidCnt + " TIDs requested: "
+ + params.tids.length);
}
}
}
@@ -1772,9 +1795,13 @@
throw new IllegalArgumentException(
"Unknown GPU headroom calculation type " + (int) params.calculationType);
}
- if (params.calculationWindowMillis < 50 || params.calculationWindowMillis > 10000) {
+ if (params.calculationWindowMillis < mSupportInfo.headroom.gpuMinCalculationWindowMillis
+ || params.calculationWindowMillis
+ > mSupportInfo.headroom.gpuMaxCalculationWindowMillis) {
throw new IllegalArgumentException(
- "Invalid GPU headroom calculation window, expected [50, 10000] but got "
+ "Invalid GPU headroom calculation window, expected ["
+ + mSupportInfo.headroom.gpuMinCalculationWindowMillis + ", "
+ + mSupportInfo.headroom.gpuMaxCalculationWindowMillis + "] but got "
+ params.calculationWindowMillis);
}
}
@@ -1807,9 +1834,15 @@
@Override
public IHintManager.HintManagerClientData
registerClient(@NonNull IHintManager.IHintManagerClient clientBinder) {
+ return getClientData();
+ }
+
+ @Override
+ public IHintManager.HintManagerClientData getClientData() {
IHintManager.HintManagerClientData out = new IHintManager.HintManagerClientData();
out.preferredRateNanos = mHintSessionPreferredRate;
out.maxGraphicsPipelineThreads = getMaxGraphicsPipelineThreadsCount();
+ out.maxCpuHeadroomThreads = DEFAULT_MAX_CPU_HEADROOM_THREADS_COUNT;
out.powerHalVersion = mPowerHalVersion;
out.supportInfo = mSupportInfo;
return out;
@@ -1838,23 +1871,40 @@
}
}
}
- pw.println("CPU Headroom Interval: " + mSupportInfo.headroom.cpuMinIntervalMillis);
- pw.println("GPU Headroom Interval: " + mSupportInfo.headroom.gpuMinIntervalMillis);
- try {
- CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
- params.usesDeviceHeadroom = true;
- CpuHeadroomResult ret = getCpuHeadroom(params);
- pw.println("CPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
- } catch (Exception e) {
- Slog.d(TAG, "Failed to dump CPU headroom", e);
- pw.println("CPU headroom: N/A");
+ pw.println("CPU Headroom Supported: " + mSupportInfo.headroom.isCpuSupported);
+ if (mSupportInfo.headroom.isCpuSupported) {
+ pw.println("CPU Headroom Interval: " + mSupportInfo.headroom.cpuMinIntervalMillis);
+ pw.println("CPU Headroom TID Max Count: " + mCpuHeadroomMaxTidCnt);
+ pw.println("CPU Headroom TID Max Count From HAL: "
+ + mSupportInfo.headroom.cpuMaxTidCount);
+ pw.println("CPU Headroom Calculation Window Range: ["
+ + mSupportInfo.headroom.cpuMinCalculationWindowMillis + ", "
+ + mSupportInfo.headroom.cpuMaxCalculationWindowMillis + "]");
+ try {
+ CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal();
+ params.usesDeviceHeadroom = true;
+ CpuHeadroomResult ret = getCpuHeadroom(params);
+ pw.println("CPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
+ } catch (Exception e) {
+ Slog.d(TAG, "Failed to dump CPU headroom", e);
+ pw.println("CPU headroom: N/A");
+ }
}
- try {
- GpuHeadroomResult ret = getGpuHeadroom(new GpuHeadroomParamsInternal());
- pw.println("GPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
- } catch (Exception e) {
- Slog.d(TAG, "Failed to dump GPU headroom", e);
- pw.println("GPU headroom: N/A");
+ pw.println("GPU Headroom Supported: " + mSupportInfo.headroom.isGpuSupported);
+ if (mSupportInfo.headroom.isGpuSupported) {
+ pw.println("GPU Headroom Interval: " + mSupportInfo.headroom.gpuMinIntervalMillis);
+ pw.println("GPU Headroom Calculation Window Range: ["
+ + mSupportInfo.headroom.gpuMinCalculationWindowMillis + ", "
+ + mSupportInfo.headroom.gpuMaxCalculationWindowMillis + "]");
+ try {
+ GpuHeadroomParamsInternal params = new GpuHeadroomParamsInternal();
+ params.calculationWindowMillis = mDefaultGpuHeadroomCalculationWindowMillis;
+ GpuHeadroomResult ret = getGpuHeadroom(params);
+ pw.println("GPU headroom: " + (ret == null ? "N/A" : ret.getGlobalHeadroom()));
+ } catch (Exception e) {
+ Slog.d(TAG, "Failed to dump GPU headroom", e);
+ pw.println("GPU headroom: N/A");
+ }
}
}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index caaf5a2..9206cce 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -2205,6 +2205,11 @@
getWakelockDurationRetriever() {
return mWakelockDurationRetriever;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return BatteryStatsImpl.this.networkStatsDelta(stats, oldStats);
+ }
}
private final PowerStatsCollectorInjector mPowerStatsCollectorInjector =
@@ -12392,83 +12397,13 @@
return networkStatsManager.getWifiUidStats();
}
- static class NetworkStatsDelta {
- int mUid;
- int mSet;
- long mRxBytes;
- long mRxPackets;
- long mTxBytes;
- long mTxPackets;
-
- public int getUid() {
- return mUid;
+ @VisibleForTesting
+ protected NetworkStats networkStatsDelta(@NonNull NetworkStats stats,
+ @Nullable NetworkStats oldStats) {
+ if (oldStats == null) {
+ return stats;
}
-
-
- public int getSet() {
- return mSet;
- }
-
- public long getRxBytes() {
- return mRxBytes;
- }
-
- public long getRxPackets() {
- return mRxPackets;
- }
-
- public long getTxBytes() {
- return mTxBytes;
- }
-
- public long getTxPackets() {
- return mTxPackets;
- }
-
- @Override
- public String toString() {
- return "NetworkStatsDelta{mUid=" + mUid + ", mSet=" + mSet + ", mRxBytes=" + mRxBytes
- + ", mRxPackets=" + mRxPackets + ", mTxBytes=" + mTxBytes + ", mTxPackets="
- + mTxPackets + '}';
- }
- }
-
- static List<NetworkStatsDelta> computeDelta(NetworkStats currentStats,
- NetworkStats lastStats) {
- List<NetworkStatsDelta> deltaList = new ArrayList<>();
- for (NetworkStats.Entry entry : currentStats) {
- NetworkStatsDelta delta = new NetworkStatsDelta();
- delta.mUid = entry.getUid();
- delta.mSet = entry.getSet();
- NetworkStats.Entry lastEntry = null;
- if (lastStats != null) {
- for (NetworkStats.Entry e : lastStats) {
- if (e.getUid() == entry.getUid() && e.getSet() == entry.getSet()
- && e.getTag() == entry.getTag()
- && e.getMetered() == entry.getMetered()
- && e.getRoaming() == entry.getRoaming()
- && e.getDefaultNetwork() == entry.getDefaultNetwork()
- /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
- lastEntry = e;
- break;
- }
- }
- }
- if (lastEntry != null) {
- delta.mRxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes());
- delta.mRxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets());
- delta.mTxBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes());
- delta.mTxPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets());
- } else {
- delta.mRxBytes = entry.getRxBytes();
- delta.mRxPackets = entry.getRxPackets();
- delta.mTxBytes = entry.getTxBytes();
- delta.mTxPackets = entry.getTxPackets();
- }
- deltaList.add(delta);
- }
-
- return deltaList;
+ return stats.subtract(oldStats);
}
/**
@@ -12486,12 +12421,12 @@
}
}
+ NetworkStats delta;
// Grab a separate lock to acquire the network stats, which may do I/O.
- List<NetworkStatsDelta> delta;
synchronized (mWifiNetworkLock) {
final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager);
if (latestStats != null) {
- delta = computeDelta(latestStats, mLastWifiNetworkStats);
+ delta = networkStatsDelta(latestStats, mLastWifiNetworkStats);
mLastWifiNetworkStats = latestStats;
} else {
delta = null;
@@ -12501,15 +12436,15 @@
}
private void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo wifiActivityEnergyInfo,
- List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+ NetworkStats networkStatsDelta, long elapsedRealtimeMs, long uptimeMs) {
// Do not populate consumed energy, because energy attribution is done by
// WifiPowerStatsProcessor.
- updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+ updateWifiBatteryStats(wifiActivityEnergyInfo, networkStatsDelta, POWER_DATA_UNAVAILABLE,
elapsedRealtimeMs, uptimeMs);
}
private void updateWifiBatteryStats(WifiActivityEnergyInfo info,
- List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+ NetworkStats delta, long consumedChargeUC, long elapsedRealtimeMs,
long uptimeMs) {
synchronized (this) {
if (!mOnBatteryInternal || mIgnoreNextExternalStats) {
@@ -12535,7 +12470,7 @@
long totalTxPackets = 0;
long totalRxPackets = 0;
if (delta != null) {
- for (NetworkStatsDelta entry : delta) {
+ for (NetworkStats.Entry entry : delta) {
if (DEBUG_ENERGY) {
Slog.d(TAG, "Wifi uid " + entry.getUid()
+ ": delta rx=" + entry.getRxBytes()
@@ -12879,11 +12814,11 @@
mLastModemActivityInfo = activityInfo;
// Grab a separate lock to acquire the network stats, which may do I/O.
- List<NetworkStatsDelta> delta = null;
+ NetworkStats delta = null;
synchronized (mModemNetworkLock) {
final NetworkStats latestStats = readMobileNetworkStatsLocked(networkStatsManager);
if (latestStats != null) {
- delta = computeDelta(latestStats, mLastModemNetworkStats);
+ delta = networkStatsDelta(latestStats, mLastModemNetworkStats);
mLastModemNetworkStats = latestStats;
}
}
@@ -12892,15 +12827,15 @@
}
private void onMobileRadioPowerStatsRetrieved(ModemActivityInfo modemActivityInfo,
- List<NetworkStatsDelta> networkStatsDeltas, long elapsedRealtimeMs, long uptimeMs) {
+ NetworkStats networkStatsDelta, long elapsedRealtimeMs, long uptimeMs) {
// Do not populate consumed energy, because energy attribution is done by
// MobileRadioPowerStatsProcessor.
- updateCellularBatteryStats(modemActivityInfo, networkStatsDeltas, POWER_DATA_UNAVAILABLE,
+ updateCellularBatteryStats(modemActivityInfo, networkStatsDelta, POWER_DATA_UNAVAILABLE,
elapsedRealtimeMs, uptimeMs);
}
private void updateCellularBatteryStats(@Nullable ModemActivityInfo deltaInfo,
- @Nullable List<NetworkStatsDelta> delta, long consumedChargeUC, long elapsedRealtimeMs,
+ @Nullable NetworkStats delta, long consumedChargeUC, long elapsedRealtimeMs,
long uptimeMs) {
// Add modem tx power to history.
addModemTxPowerToHistory(deltaInfo, elapsedRealtimeMs, uptimeMs);
@@ -13003,7 +12938,7 @@
long totalRxPackets = 0;
long totalTxPackets = 0;
if (delta != null) {
- for (NetworkStatsDelta entry : delta) {
+ for (NetworkStats.Entry entry : delta) {
if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
continue;
}
@@ -13044,7 +12979,7 @@
// Now distribute proportional blame to the apps that did networking.
long totalPackets = totalRxPackets + totalTxPackets;
if (totalPackets > 0) {
- for (NetworkStatsDelta entry : delta) {
+ for (NetworkStats.Entry entry : delta) {
if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
continue;
}
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
index cbd6fab..f971e2e 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsCollector.java
@@ -38,7 +38,6 @@
import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.LongSupplier;
@@ -71,7 +70,7 @@
interface Observer {
void onMobileRadioPowerStatsRetrieved(
@Nullable ModemActivityInfo modemActivityDelta,
- @Nullable List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas,
+ @Nullable NetworkStats networkStatsDeltas,
long elapsedRealtimeMs, long uptimeMs);
}
@@ -86,6 +85,8 @@
TelephonyManager getTelephonyManager();
LongSupplier getCallDurationSupplier();
LongSupplier getPhoneSignalScanDurationSupplier();
+
+ NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats);
}
private final Injector mInjector;
@@ -190,7 +191,7 @@
mPowerStats.uidStats.clear();
ModemActivityInfo modemActivityDelta = collectModemActivityInfo();
- List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
+ NetworkStats networkStatsDeltas = collectNetworkStats();
mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
@@ -288,17 +289,15 @@
return deltaInfo;
}
- private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
+ private NetworkStats collectNetworkStats() {
NetworkStats networkStats = mNetworkStatsSupplier.get();
if (networkStats == null) {
return null;
}
- List<BatteryStatsImpl.NetworkStatsDelta> delta =
- BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats);
+ NetworkStats delta = mInjector.networkStatsDelta(networkStats, mLastNetworkStats);
mLastNetworkStats = networkStats;
- for (int i = delta.size() - 1; i >= 0; i--) {
- BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i);
+ for (NetworkStats.Entry uidDelta : delta) {
long rxBytes = uidDelta.getRxBytes();
long txBytes = uidDelta.getTxBytes();
long rxPackets = uidDelta.getRxPackets();
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
index 1fdeac9..5440bcf 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -31,7 +31,6 @@
import com.android.server.power.stats.format.WifiPowerStatsLayout;
import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@@ -43,7 +42,7 @@
interface Observer {
void onWifiPowerStatsRetrieved(WifiActivityEnergyInfo info,
- List<BatteryStatsImpl.NetworkStatsDelta> delta, long elapsedRealtimeMs,
+ NetworkStats delta, long elapsedRealtimeMs,
long uptimeMs);
}
@@ -66,6 +65,8 @@
Supplier<NetworkStats> getWifiNetworkStatsSupplier();
WifiManager getWifiManager();
WifiStatsRetriever getWifiStatsRetriever();
+
+ NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats);
}
private final Injector mInjector;
@@ -161,7 +162,7 @@
} else {
collectWifiActivityStats();
}
- List<BatteryStatsImpl.NetworkStatsDelta> networkStatsDeltas = collectNetworkStats();
+ NetworkStats networkStatsDeltas = collectNetworkStats();
collectWifiScanTime();
mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
@@ -227,17 +228,15 @@
mPowerStats.durationMs = duration;
}
- private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
+ private NetworkStats collectNetworkStats() {
NetworkStats networkStats = mNetworkStatsSupplier.get();
if (networkStats == null) {
return null;
}
- List<BatteryStatsImpl.NetworkStatsDelta> delta =
- BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats);
+ NetworkStats delta = mInjector.networkStatsDelta(networkStats, mLastNetworkStats);
mLastNetworkStats = networkStats;
- for (int i = delta.size() - 1; i >= 0; i--) {
- BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i);
+ for (NetworkStats.Entry uidDelta : delta) {
long rxBytes = uidDelta.getRxBytes();
long txBytes = uidDelta.getTxBytes();
long rxPackets = uidDelta.getRxPackets();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5dbdeff..093df8c 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -367,7 +367,6 @@
import com.android.internal.os.TimeoutRecord;
import com.android.internal.os.TransferPipe;
import com.android.internal.policy.AttributeCache;
-import com.android.internal.policy.PhoneWindow;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
@@ -2027,8 +2026,8 @@
|| ent.array.getBoolean(R.styleable.Window_windowShowWallpaper, false);
mStyleFillsParent = mOccludesParent;
mNoDisplay = ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false);
- mOptOutEdgeToEdge = PhoneWindow.isOptingOutEdgeToEdgeEnforcement(
- aInfo.applicationInfo, false /* local */, ent.array);
+ mOptOutEdgeToEdge = ent.array.getBoolean(
+ R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false);
} else {
mStyleFillsParent = mOccludesParent = true;
mNoDisplay = false;
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 98ed6f7..54ae80c 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -103,6 +103,8 @@
// again, so that the control with leash can be eventually dispatched
if (!mGivenInsetsReady && isServerVisible() && !givenInsetsPending
&& mControlTarget != null) {
+ ProtoLog.d(WM_DEBUG_IME,
+ "onPostLayout: IME control ready to be dispatched, ws=%s", ws);
mGivenInsetsReady = true;
ImeTracker.forLogging().onProgress(mStatsToken,
ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
@@ -118,6 +120,8 @@
ImeTracker.PHASE_WM_POST_LAYOUT_NOTIFY_CONTROLS_CHANGED);
mStatsToken = null;
} else if (wasServerVisible && !isServerVisible()) {
+ ProtoLog.d(WM_DEBUG_IME, "onPostLayout: setImeShowing(false) was: %s, ws=%s",
+ isImeShowing(), ws);
setImeShowing(false);
}
}
@@ -621,6 +625,7 @@
// request (cancelling the initial show) or hide request (aborting the initial show).
logIsScheduledAndReadyToShowIme(!visible /* aborted */);
}
+ ProtoLog.d(WM_DEBUG_IME, "receiveImeStatsToken: visible=%s", visible);
if (visible) {
ImeTracker.forLogging().onCancelled(
mStatsToken, ImeTracker.PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 9ab9a8f..c5d42ad 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1787,9 +1787,11 @@
SignedConfigService.registerUpdateReceiver(mSystemContext);
t.traceEnd();
- t.traceBegin("AppIntegrityService");
- mSystemServiceManager.startService(AppIntegrityManagerService.class);
- t.traceEnd();
+ if (!android.server.Flags.removeAppIntegrityManagerService()) {
+ t.traceBegin("AppIntegrityService");
+ mSystemServiceManager.startService(AppIntegrityManagerService.class);
+ t.traceEnd();
+ }
t.traceBegin("StartLogcatManager");
mSystemServiceManager.startService(LogcatManagerService.class);
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 0d222fb..4d021ec 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -51,4 +51,11 @@
description: "Remove GameManagerService from Wear"
bug: "340929737"
is_fixed_read_only: true
+}
+
+flag {
+ name: "remove_app_integrity_manager_service"
+ namespace: "package_manager_service"
+ description: "Remove AppIntegrityManagerService"
+ bug: "364200023"
}
\ No newline at end of file
diff --git a/services/robotests/Android.bp b/services/robotests/Android.bp
index 6c4158e..8e0eb6b 100644
--- a/services/robotests/Android.bp
+++ b/services/robotests/Android.bp
@@ -63,7 +63,6 @@
instrumentation_for: "FrameworksServicesLib",
- upstream: true,
strict_mode: false,
}
diff --git a/services/robotests/backup/Android.bp b/services/robotests/backup/Android.bp
index 3ace3fb..95b38e5 100644
--- a/services/robotests/backup/Android.bp
+++ b/services/robotests/backup/Android.bp
@@ -66,7 +66,6 @@
instrumentation_for: "BackupFrameworksServicesLib",
- upstream: true,
strict_mode: false,
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index e6ff506..da58aa1 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -86,6 +86,7 @@
"src/com/android/server/inputmethod/**/ClientControllerTest.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
android_test {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 9e96800..4a09802 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -258,7 +258,6 @@
mService.mOomAdjuster = mService.mProcessStateController.getOomAdjuster();
mService.mOomAdjuster.mAdjSeq = 10000;
mService.mWakefulness = new AtomicInteger(PowerManagerInternal.WAKEFULNESS_AWAKE);
- mSetFlagsRule.enableFlags(Flags.FLAG_NEW_FGS_RESTRICTION_LOGIC);
mUiTierSize = mService.mConstants.TIERED_CACHED_ADJ_UI_TIER_SIZE;
mFirstNonUiCachedAdj = sFirstUiCachedAdj + mUiTierSize;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
index 00b911b..cd3683b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsCollectorTest.java
@@ -158,6 +158,11 @@
public LongSupplier getPhoneSignalScanDurationSupplier() {
return mScanDurationSupplier;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 4b6fcc3..8a081f8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -283,6 +283,11 @@
protected void updateBatteryPropertiesLocked() {
}
+ @Override
+ protected NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
+
public static class DummyExternalStatsSync implements ExternalStatsSync {
public int flags = 0;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java b/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java
new file mode 100644
index 0000000..21be654
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/NetworkStatsTestUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.power.stats;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.NetworkStats;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NetworkStatsTestUtils {
+ /**
+ * Equivalent to NetworkStats.subtract, reimplementing the method for Ravenwood tests.
+ */
+ @NonNull
+ public static NetworkStats networkStatsDelta(@NonNull NetworkStats currentStats,
+ @Nullable NetworkStats lastStats) {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ if (lastStats == null) {
+ return currentStats;
+ }
+ return currentStats.subtract(lastStats);
+ }
+
+ List<NetworkStats.Entry> entries = new ArrayList<>();
+ for (NetworkStats.Entry entry : currentStats) {
+ NetworkStats.Entry lastEntry = null;
+ int uid = entry.getUid();
+ if (lastStats != null) {
+ for (NetworkStats.Entry e : lastStats) {
+ if (e.getUid() == uid && e.getSet() == entry.getSet()
+ && e.getTag() == entry.getTag()
+ && e.getMetered() == entry.getMetered()
+ && e.getRoaming() == entry.getRoaming()
+ && e.getDefaultNetwork() == entry.getDefaultNetwork()
+ /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
+ lastEntry = e;
+ break;
+ }
+ }
+ }
+ long rxBytes, rxPackets, txBytes, txPackets;
+ if (lastEntry != null) {
+ rxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes());
+ rxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets());
+ txBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes());
+ txPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets());
+ } else {
+ rxBytes = entry.getRxBytes();
+ rxPackets = entry.getRxPackets();
+ txBytes = entry.getTxBytes();
+ txPackets = entry.getTxPackets();
+ }
+
+ NetworkStats.Entry uidEntry = mock(NetworkStats.Entry.class);
+ when(uidEntry.getUid()).thenReturn(uid);
+ when(uidEntry.getRxBytes()).thenReturn(rxBytes);
+ when(uidEntry.getRxPackets()).thenReturn(rxPackets);
+ when(uidEntry.getTxBytes()).thenReturn(txBytes);
+ when(uidEntry.getTxPackets()).thenReturn(txPackets);
+
+ entries.add(uidEntry);
+ }
+ NetworkStats delta = mock(NetworkStats.class);
+ when(delta.iterator()).thenAnswer(inv -> entries.iterator());
+ return delta;
+ }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
index 8b5e6ee..a26b2c9 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java
@@ -168,6 +168,11 @@
public WifiManager getWifiManager() {
return mWifiManager;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
index 4ed44a0..6acd368 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/MobileRadioPowerStatsProcessorTest.java
@@ -56,6 +56,7 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MobileRadioPowerStatsCollector;
+import com.android.server.power.stats.NetworkStatsTestUtils;
import com.android.server.power.stats.PowerStatsCollector;
import com.android.server.power.stats.PowerStatsUidResolver;
import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
@@ -152,6 +153,11 @@
public LongSupplier getPhoneSignalScanDurationSupplier() {
return mScanDurationSupplier;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
index 535f2da..a20274f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/PhoneCallPowerStatsProcessorTest.java
@@ -43,6 +43,7 @@
import com.android.internal.os.Clock;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MobileRadioPowerStatsCollector;
+import com.android.server.power.stats.NetworkStatsTestUtils;
import com.android.server.power.stats.PowerStatsCollector;
import com.android.server.power.stats.PowerStatsUidResolver;
import com.android.server.power.stats.format.PowerStatsLayout;
@@ -135,6 +136,11 @@
public LongSupplier getPhoneSignalScanDurationSupplier() {
return mScanDurationSupplier;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
index 1e09769..bd92a84 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/processor/WifiPowerStatsProcessorTest.java
@@ -56,6 +56,7 @@
import com.android.internal.os.PowerProfile;
import com.android.server.power.stats.BatteryUsageStatsRule;
import com.android.server.power.stats.MockBatteryStatsImpl;
+import com.android.server.power.stats.NetworkStatsTestUtils;
import com.android.server.power.stats.PowerStatsCollector;
import com.android.server.power.stats.PowerStatsUidResolver;
import com.android.server.power.stats.WifiPowerStatsCollector;
@@ -178,6 +179,11 @@
public WifiStatsRetriever getWifiStatsRetriever() {
return mWifiStatsRetriever;
}
+
+ @Override
+ public NetworkStats networkStatsDelta(NetworkStats stats, NetworkStats oldStats) {
+ return NetworkStatsTestUtils.networkStatsDelta(stats, oldStats);
+ }
};
@Before
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 2fe6918..6411463 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -94,6 +94,7 @@
import android.os.Message;
import android.os.PowerManagerInternal;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.IStorageManager;
@@ -181,14 +182,12 @@
Intent.ACTION_USER_STARTING);
private static final Set<Integer> START_FOREGROUND_USER_MESSAGE_CODES = newHashSet(
- 0, // for startUserInternalOnHandler
REPORT_USER_SWITCH_MSG,
USER_SWITCH_TIMEOUT_MSG,
USER_START_MSG,
USER_CURRENT_MSG);
private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet(
- 0, // for startUserInternalOnHandler
USER_START_MSG,
REPORT_LOCKED_BOOT_COMPLETE_MSG);
@@ -376,7 +375,7 @@
// and the cascade effect goes on...). In fact, a better approach would to not assert the
// binder calls, but their side effects (in this case, that the user is stopped right away)
assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes())
- .containsExactly(/* for startUserInternalOnHandler */ 0, USER_START_MSG);
+ .containsExactly(USER_START_MSG);
}
private void startUserAssertions(
@@ -419,17 +418,12 @@
@Test
public void testDispatchUserSwitch() throws RemoteException {
// Prepare mock observer and register it
- IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
- when(observer.asBinder()).thenReturn(new Binder());
- doAnswer(invocation -> {
- IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
- callback.sendResult(null);
- return null;
- }).when(observer).onUserSwitching(anyInt(), any());
- mUserController.registerUserSwitchObserver(observer, "mock");
+ IUserSwitchObserver observer = registerUserSwitchObserver(
+ /* replyToOnBeforeUserSwitchingCallback= */ true,
+ /* replyToOnUserSwitchingCallback= */ true);
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
- verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
+ verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any());
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -454,13 +448,13 @@
@Test
public void testDispatchUserSwitchBadReceiver() throws RemoteException {
- // Prepare mock observer which doesn't notify the callback and register it
- IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
- when(observer.asBinder()).thenReturn(new Binder());
- mUserController.registerUserSwitchObserver(observer, "mock");
+ // Prepare mock observer which doesn't notify the onUserSwitching callback and register it
+ IUserSwitchObserver observer = registerUserSwitchObserver(
+ /* replyToOnBeforeUserSwitchingCallback= */ true,
+ /* replyToOnUserSwitchingCallback= */ false);
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
- verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID));
+ verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any());
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
assertNotNull(reportMsg);
UserState userState = (UserState) reportMsg.obj;
@@ -551,7 +545,6 @@
expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG);
if (backgroundUserStopping) {
expectedCodes.add(CLEAR_USER_JOURNEY_SESSION_MSG);
- expectedCodes.add(0); // this is for directly posting in stopping.
}
if (expectScheduleBackgroundUserStopping) {
expectedCodes.add(SCHEDULED_STOP_BACKGROUND_USER_MSG);
@@ -567,9 +560,9 @@
@Test
public void testDispatchUserSwitchComplete() throws RemoteException {
// Prepare mock observer and register it
- IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
- when(observer.asBinder()).thenReturn(new Binder());
- mUserController.registerUserSwitchObserver(observer, "mock");
+ IUserSwitchObserver observer = registerUserSwitchObserver(
+ /* replyToOnBeforeUserSwitchingCallback= */ true,
+ /* replyToOnUserSwitchingCallback= */ true);
// Start user -- this will update state of mUserController
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG);
@@ -1752,6 +1745,29 @@
verify(mInjector, never()).onSystemUserVisibilityChanged(anyBoolean());
}
+ private IUserSwitchObserver registerUserSwitchObserver(
+ boolean replyToOnBeforeUserSwitchingCallback, boolean replyToOnUserSwitchingCallback)
+ throws RemoteException {
+ IUserSwitchObserver observer = mock(IUserSwitchObserver.class);
+ when(observer.asBinder()).thenReturn(new Binder());
+ if (replyToOnBeforeUserSwitchingCallback) {
+ doAnswer(invocation -> {
+ IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+ callback.sendResult(null);
+ return null;
+ }).when(observer).onBeforeUserSwitching(anyInt(), any());
+ }
+ if (replyToOnUserSwitchingCallback) {
+ doAnswer(invocation -> {
+ IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1];
+ callback.sendResult(null);
+ return null;
+ }).when(observer).onUserSwitching(anyInt(), any());
+ }
+ mUserController.registerUserSwitchObserver(observer, "mock");
+ return observer;
+ }
+
// Should be public to allow mocking
private static class TestInjector extends UserController.Injector {
public final TestHandler mHandler;
@@ -1957,6 +1973,7 @@
* fix this, but in the meantime, this is your warning.
*/
private final List<Message> mMessages = new ArrayList<>();
+ private final List<Runnable> mPendingCallbacks = new ArrayList<>();
TestHandler(Looper looper) {
super(looper);
@@ -1989,14 +2006,24 @@
@Override
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
- Message copy = new Message();
- copy.copyFrom(msg);
- mMessages.add(copy);
- if (msg.getCallback() != null) {
- msg.getCallback().run();
+ if (msg.getCallback() == null) {
+ Message copy = new Message();
+ copy.copyFrom(msg);
+ mMessages.add(copy);
+ } else {
+ if (SystemClock.uptimeMillis() >= uptimeMillis) {
+ msg.getCallback().run();
+ } else {
+ mPendingCallbacks.add(msg.getCallback());
+ }
msg.setCallback(null);
}
return super.sendMessageAtTime(msg, uptimeMillis);
}
+
+ private void runPendingCallbacks() {
+ mPendingCallbacks.forEach(Runnable::run);
+ mPendingCallbacks.clear();
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 0244164..4f55111 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -887,6 +887,21 @@
systemAudioModeRequest_fromAudioSystem);
}
+ @Test
+ public void addAndStartAction_remove() throws Exception {
+ // utilize callback test to test if addAndStartAction(action, remove)
+ TestCallback callback = new TestCallback();
+
+ mHdmiCecLocalDeviceAudioSystem.setArcStatus(true);
+ mHdmiCecLocalDeviceAudioSystem.addAndStartAction(
+ new ArcTerminationActionFromAvr(mHdmiCecLocalDeviceAudioSystem, callback),
+ true);
+
+ mTestLooper.dispatchAll();
+ assertThat(mHdmiCecLocalDeviceAudioSystem.getActions(
+ ArcTerminationActionFromAvr.class).size()).isEqualTo(1);
+ }
+
private static class TestCallback extends IHdmiControlCallback.Stub {
private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
index 72fa949..085ef53b 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
@@ -196,6 +196,8 @@
},
ActorState.INVALID_OVERLAYABLE_ACTOR_NAME withCases {
fun TestState.mockActor(actorUri: String) {
+ namedActorsMap = mapOf(VALID_NAMESPACE to
+ mapOf(VALID_ACTOR_NAME to VALID_ACTOR_PKG))
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, actorUri)
}
failure("wrongScheme") {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index b6e393d..03d9042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -342,8 +342,8 @@
public void testCancelRemoteAnimationWhenFreeze() {
final DisplayContent dc = createNewDisplay(Display.STATE_ON);
doReturn(false).when(dc).onDescendantOrientationChanged(any());
- final WindowState exitingAppWindow = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
- dc, "exiting app");
+ final WindowState exitingAppWindow = newWindowBuilder("exiting app",
+ TYPE_BASE_APPLICATION).setDisplay(dc).build();
final ActivityRecord exitingActivity = exitingAppWindow.mActivityRecord;
// Wait until everything in animation handler get executed to prevent the exiting window
// from being removed during WindowSurfacePlacer Traversal.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
index 14276ae..7033d79 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentDeferredUpdateTests.java
@@ -266,10 +266,10 @@
mSetFlagsRule.enableFlags(Flags.FLAG_WAIT_FOR_TRANSITION_ON_DISPLAY_SWITCH);
prepareSecondaryDisplay();
- final WindowState defaultDisplayWindow = createWindow(/* parent= */ null,
- TYPE_BASE_APPLICATION, mDisplayContent, "DefaultDisplayWindow");
- final WindowState secondaryDisplayWindow = createWindow(/* parent= */ null,
- TYPE_BASE_APPLICATION, mSecondaryDisplayContent, "SecondaryDisplayWindow");
+ final WindowState defaultDisplayWindow = newWindowBuilder("DefaultDisplayWindow",
+ TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
+ final WindowState secondaryDisplayWindow = newWindowBuilder("SecondaryDisplayWindow",
+ TYPE_BASE_APPLICATION).setDisplay(mSecondaryDisplayContent).build();
makeWindowVisibleAndNotDrawn(defaultDisplayWindow, secondaryDisplayWindow);
// Mark as display switching only for the default display as we filter out
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index bd15bc4..347d1bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -379,13 +379,11 @@
assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer);
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
- final WindowState secondActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity,
- "firstActivityWin");
+ final WindowState secondActivityWin = newWindowBuilder("secondActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build();
spyOn(secondActivityWin);
// firstActivityWin should be the target
@@ -424,13 +422,11 @@
setupImeWindow();
final DisplayArea.Tokens imeContainer = mDisplay.getImeContainer();
final WindowToken imeToken = tokenOfType(TYPE_INPUT_METHOD);
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
- final WindowState secondActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mSecondActivity,
- "secondActivityWin");
+ final WindowState secondActivityWin = newWindowBuilder("secondActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mSecondActivity).build();
spyOn(secondActivityWin);
// firstActivityWin should be the target
@@ -464,9 +460,8 @@
assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
assertThat(mDisplay.findAreaForTokenInLayer(imeToken)).isEqualTo(imeContainer);
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
// firstActivityWin should be the target
doReturn(true).when(firstActivityWin).canBeImeTarget();
@@ -499,9 +494,8 @@
assertThat(imeContainer.getRootDisplayArea()).isEqualTo(mDisplay);
// firstActivityWin should be the target
- final WindowState firstActivityWin =
- createWindow(null /* parent */, TYPE_APPLICATION_STARTING, mFirstActivity,
- "firstActivityWin");
+ final WindowState firstActivityWin = newWindowBuilder("firstActivityWin",
+ TYPE_APPLICATION_STARTING).setWindowToken(mFirstActivity).build();
spyOn(firstActivityWin);
doReturn(true).when(firstActivityWin).canBeImeTarget();
WindowState imeTarget = mDisplay.computeImeTarget(true /* updateImeTarget */);
@@ -560,8 +554,8 @@
}
private void setupImeWindow() {
- final WindowState imeWindow = createWindow(null /* parent */,
- TYPE_INPUT_METHOD, mDisplay, "mImeWindow");
+ final WindowState imeWindow = newWindowBuilder("mImeWindow", TYPE_INPUT_METHOD).setDisplay(
+ mDisplay).build();
imeWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
mDisplay.mInputMethodWindow = imeWindow;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index dc4adcc..2997173 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -878,8 +878,10 @@
.build();
final ActivityRecord activity0 = tf0.getTopMostActivity();
final ActivityRecord activity1 = tf1.getTopMostActivity();
- final WindowState win0 = createWindow(null, TYPE_BASE_APPLICATION, activity0, "win0");
- final WindowState win1 = createWindow(null, TYPE_BASE_APPLICATION, activity1, "win1");
+ final WindowState win0 = newWindowBuilder("win0", TYPE_BASE_APPLICATION).setWindowToken(
+ activity0).build();
+ final WindowState win1 = newWindowBuilder("win1", TYPE_BASE_APPLICATION).setWindowToken(
+ activity1).build();
doReturn(false).when(mDisplayContent).shouldImeAttachedToApp();
mDisplayContent.setImeInputTarget(win0);
@@ -1174,8 +1176,8 @@
}
private WindowState createAppWindow(ActivityRecord app, String name) {
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, app, name,
- 0 /* ownerId */, false /* ownerCanAddInternalSystemWindow */, new TestIWindow());
+ final WindowState win = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken(
+ app).setClientWindow(new TestIWindow()).build();
mWm.mWindowMap.put(win.mClient.asBinder(), win);
return win;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
index f145b40..f9250f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotCacheTest.java
@@ -63,7 +63,7 @@
@Test
public void testAppRemoved() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), createSnapshot());
assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
mCache.onAppRemoved(window.mActivityRecord);
@@ -72,7 +72,7 @@
@Test
public void testAppDied() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), createSnapshot());
assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
mCache.onAppDied(window.mActivityRecord);
@@ -81,7 +81,7 @@
@Test
public void testTaskRemoved() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), createSnapshot());
assertNotNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
mCache.onIdRemoved(window.getTask().mTaskId);
@@ -90,7 +90,7 @@
@Test
public void testReduced_notCached() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
@@ -105,7 +105,7 @@
@Test
public void testRestoreFromDisk() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
@@ -117,7 +117,7 @@
@Test
public void testClearCache() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mCache.putSnapshot(window.getTask(), mSnapshot);
assertEquals(mSnapshot, mCache.getSnapshot(window.getTask().mTaskId,
false /* isLowResolution */));
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
index c6b2a6b..1bca53a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java
@@ -74,8 +74,8 @@
@Test
public void testGetClosingApps_closing() {
- final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
- "closingWindow");
+ final WindowState closingWindow = newWindowBuilder("closingWindow",
+ FIRST_APPLICATION_WINDOW).build();
closingWindow.mActivityRecord.commitVisibility(
false /* visible */, true /* performLayout */);
final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
@@ -88,8 +88,8 @@
@Test
public void testGetClosingApps_notClosing() {
- final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
- "closingWindow");
+ final WindowState closingWindow = newWindowBuilder("closingWindow",
+ FIRST_APPLICATION_WINDOW).build();
final WindowState openingWindow = createAppWindow(closingWindow.getTask(),
FIRST_APPLICATION_WINDOW, "openingWindow");
closingWindow.mActivityRecord.commitVisibility(
@@ -105,8 +105,8 @@
@Test
public void testGetClosingApps_skipClosingAppsSnapshotTasks() {
- final WindowState closingWindow = createWindow(null, FIRST_APPLICATION_WINDOW,
- "closingWindow");
+ final WindowState closingWindow = newWindowBuilder("closingWindow",
+ FIRST_APPLICATION_WINDOW).build();
closingWindow.mActivityRecord.commitVisibility(
false /* visible */, true /* performLayout */);
final ArraySet<ActivityRecord> closingApps = new ArraySet<>();
@@ -133,19 +133,19 @@
@Test
public void testGetSnapshotMode() {
- final WindowState disabledWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "disabledWindow");
+ final WindowState disabledWindow = newWindowBuilder("disabledWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
disabledWindow.mActivityRecord.setRecentsScreenshotEnabled(false);
assertEquals(SNAPSHOT_MODE_APP_THEME,
mWm.mTaskSnapshotController.getSnapshotMode(disabledWindow.getTask()));
- final WindowState normalWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ final WindowState normalWindow = newWindowBuilder("normalWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
assertEquals(SNAPSHOT_MODE_REAL,
mWm.mTaskSnapshotController.getSnapshotMode(normalWindow.getTask()));
- final WindowState secureWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "secureWindow");
+ final WindowState secureWindow = newWindowBuilder("secureWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
secureWindow.mAttrs.flags |= FLAG_SECURE;
assertEquals(SNAPSHOT_MODE_APP_THEME,
mWm.mTaskSnapshotController.getSnapshotMode(secureWindow.getTask()));
@@ -297,8 +297,8 @@
spyOn(mWm.mTaskSnapshotController);
doReturn(false).when(mWm.mTaskSnapshotController).shouldDisableSnapshots();
- final WindowState normalWindow = createWindow(null,
- FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow");
+ final WindowState normalWindow = newWindowBuilder("normalWindow",
+ FIRST_APPLICATION_WINDOW).setDisplay(mDisplayContent).build();
final TaskSnapshot snapshot = new TaskSnapshotPersisterTestBase.TaskSnapshotBuilder()
.setTopActivityComponent(normalWindow.mActivityRecord.mActivityComponent).build();
doReturn(snapshot).when(mWm.mTaskSnapshotController).snapshot(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
index 9bde066..51ea498 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotLowResDisabledTest.java
@@ -41,7 +41,7 @@
* Test class for {@link TaskSnapshotPersister} and {@link AppSnapshotLoader}
*
* Build/Install/Run:
- * atest TaskSnapshotPersisterLoaderTest
+ * atest TaskSnapshotLowResDisabledTest
*/
@MediumTest
@Presubmit
@@ -126,7 +126,7 @@
@Test
public void testReduced_notCached() {
- final WindowState window = createWindow(null, FIRST_APPLICATION_WINDOW, "window");
+ final WindowState window = newWindowBuilder("window", FIRST_APPLICATION_WINDOW).build();
mPersister.persistSnapshot(window.getTask().mTaskId, mWm.mCurrentUserId, createSnapshot());
mSnapshotPersistQueue.waitForQueueEmpty();
assertNull(mCache.getSnapshot(window.getTask().mTaskId, false /* isLowResolution */));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 1fa6578..5ed2df3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -504,8 +504,8 @@
assertTrue(child.isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION));
assertFalse(child.isAnimating(PARENTS, ANIMATION_TYPE_SCREEN_ROTATION));
- final WindowState windowState = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
- mDisplayContent, "TestWindowState");
+ final WindowState windowState = newWindowBuilder("TestWindowState",
+ TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
WindowContainer parent = windowState.getParent();
spyOn(windowState.mSurfaceAnimator);
doReturn(true).when(windowState.mSurfaceAnimator).isAnimating();
@@ -1045,8 +1045,8 @@
// An animating window with mRemoveOnExit can be removed by handleCompleteDeferredRemoval
// once it no longer animates.
- final WindowState exitingWindow = createWindow(null, TYPE_APPLICATION_OVERLAY,
- displayContent, "exiting window");
+ final WindowState exitingWindow = newWindowBuilder("exiting window",
+ TYPE_APPLICATION_OVERLAY).setDisplay(displayContent).build();
exitingWindow.startAnimation(exitingWindow.getPendingTransaction(),
mock(AnimationAdapter.class), false /* hidden */,
SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION);
@@ -1063,7 +1063,7 @@
final ActivityRecord r = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setDisplay(displayContent).build().getTopMostActivity();
// Add a window and make the activity animating so the removal of activity is deferred.
- createWindow(null, TYPE_BASE_APPLICATION, r, "win");
+ newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(r).build();
doReturn(true).when(r).isAnimating(anyInt(), anyInt());
displayContent.remove();
@@ -1216,7 +1216,8 @@
public void testFreezeInsets() {
final Task task = createTask(mDisplayContent);
final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(
+ activity).build();
// Set visibility to false, verify the main window of the task will be set the frozen
// insets state immediately.
@@ -1233,7 +1234,8 @@
final Task rootTask = createTask(mDisplayContent);
final Task task = createTaskInRootTask(rootTask, 0 /* userId */);
final ActivityRecord activity = createActivityRecord(mDisplayContent, task);
- final WindowState win = createWindow(null, TYPE_BASE_APPLICATION, activity, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_BASE_APPLICATION).setWindowToken(
+ activity).build();
task.getDisplayContent().prepareAppTransition(TRANSIT_CLOSE);
spyOn(win);
doReturn(true).when(task).okToAnimate();
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
index 72935cb..8606581 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTraversalTests.java
@@ -49,9 +49,10 @@
@SetupWindows(addWindows = { W_DOCK_DIVIDER, W_INPUT_METHOD })
@Test
public void testDockedDividerPosition() {
- final WindowState splitScreenWindow = createWindow(null,
- WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
- mDisplayContent, "splitScreenWindow");
+ final WindowState splitScreenWindow = newWindowBuilder("splitScreenWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+ mDisplayContent).build();
mDisplayContent.setImeLayeringTarget(splitScreenWindow);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 50e0e18..513ba1d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -154,9 +154,11 @@
@Test
public void testIsParentWindowHidden() {
- final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
- final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+ final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
// parentWindow is initially set to hidden.
assertTrue(parentWindow.mHidden);
@@ -172,10 +174,12 @@
@Test
public void testIsChildWindow() {
- final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
- final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
- final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+ final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build();
assertFalse(parentWindow.isChildWindow());
assertTrue(child1.isChildWindow());
@@ -185,12 +189,15 @@
@Test
public void testHasChild() {
- final WindowState win1 = createWindow(null, TYPE_APPLICATION, "win1");
- final WindowState win11 = createWindow(win1, FIRST_SUB_WINDOW, "win11");
- final WindowState win12 = createWindow(win1, FIRST_SUB_WINDOW, "win12");
- final WindowState win2 = createWindow(null, TYPE_APPLICATION, "win2");
- final WindowState win21 = createWindow(win2, FIRST_SUB_WINDOW, "win21");
- final WindowState randomWindow = createWindow(null, TYPE_APPLICATION, "randomWindow");
+ final WindowState win1 = newWindowBuilder("win1", TYPE_APPLICATION).build();
+ final WindowState win11 = newWindowBuilder("win11", FIRST_SUB_WINDOW).setParent(
+ win1).build();
+ final WindowState win12 = newWindowBuilder("win12", FIRST_SUB_WINDOW).setParent(
+ win1).build();
+ final WindowState win2 = newWindowBuilder("win2", TYPE_APPLICATION).build();
+ final WindowState win21 = newWindowBuilder("win21", FIRST_SUB_WINDOW).setParent(
+ win2).build();
+ final WindowState randomWindow = newWindowBuilder("randomWindow", TYPE_APPLICATION).build();
assertTrue(win1.hasChild(win11));
assertTrue(win1.hasChild(win12));
@@ -206,9 +213,11 @@
@Test
public void testGetParentWindow() {
- final WindowState parentWindow = createWindow(null, TYPE_APPLICATION, "parentWindow");
- final WindowState child1 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(parentWindow, FIRST_SUB_WINDOW, "child2");
+ final WindowState parentWindow = newWindowBuilder("parentWindow", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ parentWindow).build();
assertNull(parentWindow.getParentWindow());
assertEquals(parentWindow, child1.getParentWindow());
@@ -217,8 +226,8 @@
@Test
public void testOverlayWindowHiddenWhenSuspended() {
- final WindowState overlayWindow = spy(createWindow(null, TYPE_APPLICATION_OVERLAY,
- "overlayWindow"));
+ final WindowState overlayWindow = spy(
+ newWindowBuilder("overlayWindow", TYPE_APPLICATION_OVERLAY).build());
overlayWindow.setHiddenWhileSuspended(true);
verify(overlayWindow).hide(true /* doAnimation */, true /* requestAnim */);
overlayWindow.setHiddenWhileSuspended(false);
@@ -227,9 +236,11 @@
@Test
public void testGetTopParentWindow() {
- final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
- final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+ final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ root).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ child1).build();
assertEquals(root, root.getTopParentWindow());
assertEquals(root, child1.getTopParentWindow());
@@ -244,7 +255,7 @@
@Test
public void testIsOnScreen_hiddenByPolicy() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
window.setHasSurface(true);
assertTrue(window.isOnScreen());
window.hide(false /* doAnimation */, false /* requestAnim */);
@@ -273,8 +284,8 @@
@Test
public void testCanBeImeTarget() {
- final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
- final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow");
+ final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build();
+ final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_INPUT_METHOD).build();
// Setting FLAG_NOT_FOCUSABLE prevents the window from being an IME target.
appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
@@ -328,16 +339,17 @@
@Test
public void testGetWindow() {
- final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
- final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild");
- final WindowState mediaOverlayChild = createWindow(root,
- TYPE_APPLICATION_MEDIA_OVERLAY, "mediaOverlayChild");
- final WindowState attachedDialogChild = createWindow(root,
- TYPE_APPLICATION_ATTACHED_DIALOG, "attachedDialogChild");
- final WindowState subPanelChild = createWindow(root,
- TYPE_APPLICATION_SUB_PANEL, "subPanelChild");
- final WindowState aboveSubPanelChild = createWindow(root,
- TYPE_APPLICATION_ABOVE_SUB_PANEL, "aboveSubPanelChild");
+ final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+ final WindowState mediaChild = newWindowBuilder("mediaChild",
+ TYPE_APPLICATION_MEDIA).setParent(root).build();
+ final WindowState mediaOverlayChild = newWindowBuilder("mediaOverlayChild",
+ TYPE_APPLICATION_MEDIA_OVERLAY).setParent(root).build();
+ final WindowState attachedDialogChild = newWindowBuilder("attachedDialogChild",
+ TYPE_APPLICATION_ATTACHED_DIALOG).setParent(root).build();
+ final WindowState subPanelChild = newWindowBuilder("subPanelChild",
+ TYPE_APPLICATION_SUB_PANEL).setParent(root).build();
+ final WindowState aboveSubPanelChild = newWindowBuilder("aboveSubPanelChild",
+ TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(root).build();
final LinkedList<WindowState> windows = new LinkedList<>();
@@ -358,7 +370,7 @@
@Test
public void testDestroySurface() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
win.mHasSurface = win.mAnimatingExit = true;
win.mWinAnimator.mSurfaceControl = mock(SurfaceControl.class);
win.onExitAnimationDone();
@@ -384,8 +396,10 @@
// Call prepareWindowToDisplayDuringRelayout for a window without FLAG_TURN_SCREEN_ON before
// calling setCurrentLaunchCanTurnScreenOn for windows with flag in the same activity.
final ActivityRecord activity = createActivityRecord(mDisplayContent);
- final WindowState first = createWindow(null, TYPE_APPLICATION, activity, "first");
- final WindowState second = createWindow(null, TYPE_APPLICATION, activity, "second");
+ final WindowState first = newWindowBuilder("first", TYPE_APPLICATION).setWindowToken(
+ activity).build();
+ final WindowState second = newWindowBuilder("second", TYPE_APPLICATION).setWindowToken(
+ activity).build();
testPrepareWindowToDisplayDuringRelayout(first, false /* expectedWakeupCalled */,
true /* expectedCurrentLaunchCanTurnScreenOn */);
@@ -423,10 +437,10 @@
// Call prepareWindowToDisplayDuringRelayout for a windows that are not children of an
// activity. Both windows have the FLAG_TURNS_SCREEN_ON so both should call wakeup
final WindowToken windowToken = createTestWindowToken(FIRST_SUB_WINDOW, mDisplayContent);
- final WindowState firstWindow = createWindow(null, TYPE_APPLICATION, windowToken,
- "firstWindow");
- final WindowState secondWindow = createWindow(null, TYPE_APPLICATION, windowToken,
- "secondWindow");
+ final WindowState firstWindow = newWindowBuilder("firstWindow",
+ TYPE_APPLICATION).setWindowToken(windowToken).build();
+ final WindowState secondWindow = newWindowBuilder("secondWindow",
+ TYPE_APPLICATION).setWindowToken(windowToken).build();
firstWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
secondWindow.mAttrs.flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
@@ -459,7 +473,7 @@
@Test
public void testCanAffectSystemUiFlags() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mActivityRecord.setVisible(true);
assertTrue(app.canAffectSystemUiFlags());
app.mActivityRecord.setVisible(false);
@@ -471,7 +485,7 @@
@Test
public void testCanAffectSystemUiFlags_starting() {
- final WindowState app = createWindow(null, TYPE_APPLICATION_STARTING, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION_STARTING).build();
app.mActivityRecord.setVisible(true);
app.mStartingData = new SnapshotStartingData(mWm, null, 0);
assertFalse(app.canAffectSystemUiFlags());
@@ -481,7 +495,7 @@
@Test
public void testCanAffectSystemUiFlags_disallow() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mActivityRecord.setVisible(true);
assertTrue(app.canAffectSystemUiFlags());
app.getTask().setCanAffectSystemUiFlags(false);
@@ -538,9 +552,11 @@
@Test
public void testIsSelfOrAncestorWindowAnimating() {
- final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
- final WindowState child1 = createWindow(root, FIRST_SUB_WINDOW, "child1");
- final WindowState child2 = createWindow(child1, FIRST_SUB_WINDOW, "child2");
+ final WindowState root = newWindowBuilder("root", TYPE_APPLICATION).build();
+ final WindowState child1 = newWindowBuilder("child1", FIRST_SUB_WINDOW).setParent(
+ root).build();
+ final WindowState child2 = newWindowBuilder("child2", FIRST_SUB_WINDOW).setParent(
+ child1).build();
assertFalse(child2.isSelfOrAncestorWindowAnimatingExit());
child2.mAnimatingExit = true;
assertTrue(child2.isSelfOrAncestorWindowAnimatingExit());
@@ -551,7 +567,7 @@
@Test
public void testDeferredRemovalByAnimating() {
- final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
+ final WindowState appWindow = newWindowBuilder("appWindow", TYPE_APPLICATION).build();
makeWindowVisible(appWindow);
spyOn(appWindow.mWinAnimator);
doReturn(true).when(appWindow.mWinAnimator).getShown();
@@ -571,8 +587,9 @@
@Test
public void testOnExitAnimationDone() {
- final WindowState parent = createWindow(null, TYPE_APPLICATION, "parent");
- final WindowState child = createWindow(parent, TYPE_APPLICATION_PANEL, "child");
+ final WindowState parent = newWindowBuilder("parent", TYPE_APPLICATION).build();
+ final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent(
+ parent).build();
final SurfaceControl.Transaction t = parent.getPendingTransaction();
child.startAnimation(t, mock(AnimationAdapter.class), false /* hidden */,
SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION);
@@ -609,7 +626,7 @@
@Test
public void testLayoutSeqResetOnReparent() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mLayoutSeq = 1;
mDisplayContent.mLayoutSeq = 1;
@@ -622,7 +639,7 @@
@Test
public void testDisplayIdUpdatedOnReparent() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
// fake a different display
app.mInputWindowHandle.setDisplayId(mDisplayContent.getDisplayId() + 1);
app.onDisplayChanged(mDisplayContent);
@@ -633,7 +650,7 @@
@Test
public void testApplyWithNextDraw() {
- final WindowState win = createWindow(null, TYPE_APPLICATION_OVERLAY, "app");
+ final WindowState win = newWindowBuilder("app", TYPE_APPLICATION_OVERLAY).build();
final SurfaceControl.Transaction[] handledT = { null };
// The normal case that the draw transaction is applied with finishing drawing.
win.applyWithNextDraw(t -> handledT[0] = t);
@@ -657,7 +674,7 @@
@Test
public void testSeamlesslyRotateWindow() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
final SurfaceControl.Transaction t = spy(StubTransaction.class);
makeWindowVisible(app);
@@ -707,7 +724,7 @@
@Test
public void testVisibilityChangeSwitchUser() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState window = newWindowBuilder("app", TYPE_APPLICATION).build();
window.mHasSurface = true;
spyOn(window);
doReturn(false).when(window).showForAllUsers();
@@ -729,8 +746,9 @@
final CompatModePackages cmp = mWm.mAtmService.mCompatModePackages;
spyOn(cmp);
doReturn(overrideScale).when(cmp).getCompatScale(anyString(), anyInt());
- final WindowState w = createWindow(null, TYPE_APPLICATION_OVERLAY, "win");
- final WindowState child = createWindow(w, TYPE_APPLICATION_PANEL, "child");
+ final WindowState w = newWindowBuilder("win", TYPE_APPLICATION_OVERLAY).build();
+ final WindowState child = newWindowBuilder("child", TYPE_APPLICATION_PANEL).setParent(
+ w).build();
assertTrue(w.hasCompatScale());
assertTrue(child.hasCompatScale());
@@ -788,7 +806,8 @@
// Child window without scale (e.g. different app) should apply inverse scale of parent.
doReturn(1f).when(cmp).getCompatScale(anyString(), anyInt());
- final WindowState child2 = createWindow(w, TYPE_APPLICATION_SUB_PANEL, "child2");
+ final WindowState child2 = newWindowBuilder("child2", TYPE_APPLICATION_SUB_PANEL).setParent(
+ w).build();
makeWindowVisible(w, child2);
clearInvocations(t);
child2.prepareSurfaces();
@@ -798,10 +817,10 @@
@SetupWindows(addWindows = { W_ABOVE_ACTIVITY, W_NOTIFICATION_SHADE })
@Test
public void testRequestDrawIfNeeded() {
- final WindowState startingApp = createWindow(null /* parent */,
- TYPE_BASE_APPLICATION, "startingApp");
- final WindowState startingWindow = createWindow(null /* parent */,
- TYPE_APPLICATION_STARTING, startingApp.mToken, "starting");
+ final WindowState startingApp = newWindowBuilder("startingApp",
+ TYPE_BASE_APPLICATION).build();
+ final WindowState startingWindow = newWindowBuilder("starting",
+ TYPE_APPLICATION_STARTING).setWindowToken(startingApp.mToken).build();
startingApp.mActivityRecord.mStartingWindow = startingWindow;
final WindowState keyguardHostWindow = mNotificationShadeWindow;
final WindowState allDrawnApp = mAppWindow;
@@ -878,7 +897,7 @@
@Test
public void testRequestResizeForBlastSync() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState win = newWindowBuilder("window", TYPE_APPLICATION).build();
makeWindowVisible(win);
makeLastConfigReportedToClient(win, true /* visible */);
win.mLayoutSeq = win.getDisplayContent().mLayoutSeq;
@@ -926,8 +945,8 @@
final Task task = createTask(mDisplayContent);
final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
- final WindowState win = createWindow(null /* parent */, TYPE_APPLICATION, embeddedActivity,
- "App window");
+ final WindowState win = newWindowBuilder("App window", TYPE_APPLICATION).setWindowToken(
+ embeddedActivity).build();
doReturn(true).when(embeddedActivity).isVisible();
embeddedActivity.setVisibleRequested(true);
makeWindowVisible(win);
@@ -949,14 +968,14 @@
@Test
public void testCantReceiveTouchWhenAppTokenHiddenRequested() {
- final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
+ final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build();
win0.mActivityRecord.setVisibleRequested(false);
assertFalse(win0.canReceiveTouchInput());
}
@Test
public void testCantReceiveTouchWhenNotFocusable() {
- final WindowState win0 = createWindow(null, TYPE_APPLICATION, "win0");
+ final WindowState win0 = newWindowBuilder("win0", TYPE_APPLICATION).build();
final Task rootTask = win0.mActivityRecord.getRootTask();
spyOn(rootTask);
when(rootTask.shouldIgnoreInput()).thenReturn(true);
@@ -969,7 +988,7 @@
@Test
public void testUpdateInputWindowHandle() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
win.mAttrs.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
win.mAttrs.flags = FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH;
final InputWindowHandle handle = new InputWindowHandle(
@@ -1026,7 +1045,7 @@
@DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
@Test
public void testTouchRegionUsesLetterboxBoundsIfTransformedBoundsAndLetterboxScrolling() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
// Transformed bounds used for size of touchable region if letterbox inner bounds are empty.
final Rect transformedBounds = new Rect(0, 0, 300, 500);
@@ -1051,7 +1070,7 @@
@DisableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
@Test
public void testTouchRegionUsesLetterboxBoundsIfNullTransformedBoundsAndLetterboxScrolling() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
// Fragment bounds used for size of touchable region if letterbox inner bounds are empty
// and Transform bounds are null.
@@ -1083,7 +1102,7 @@
@EnableFlags(Flags.FLAG_SCROLLING_FROM_LETTERBOX)
@Test
public void testTouchRegionUsesTransformedBoundsIfLetterboxScrolling() {
- final WindowState win = createWindow(null, TYPE_APPLICATION, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).build();
// Transformed bounds used for size of touchable region if letterbox inner bounds are empty.
final Rect transformedBounds = new Rect(0, 0, 300, 500);
@@ -1109,7 +1128,7 @@
public void testHasActiveVisibleWindow() {
final int uid = ActivityBuilder.DEFAULT_FAKE_UID;
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app", uid);
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).setOwnerId(uid).build();
app.mActivityRecord.setVisible(false);
app.mActivityRecord.setVisibility(false);
assertFalse(mAtm.hasActiveVisibleWindow(uid));
@@ -1120,15 +1139,17 @@
// Make the activity invisible and add a visible toast. The uid should have no active
// visible window because toast can be misused by legacy app to bypass background check.
app.mActivityRecord.setVisibility(false);
- final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay", uid);
- final WindowState toast = createWindow(null, TYPE_TOAST, app.mToken, "toast", uid);
+ final WindowState overlay = newWindowBuilder("overlay",
+ TYPE_APPLICATION_OVERLAY).setOwnerId(uid).build();
+ final WindowState toast = newWindowBuilder("toast", TYPE_TOAST).setWindowToken(
+ app.mToken).setOwnerId(uid).build();
toast.onSurfaceShownChanged(true);
assertFalse(mAtm.hasActiveVisibleWindow(uid));
// Though starting window should belong to system. Make sure it is ignored to avoid being
// allow-list unexpectedly, see b/129563343.
- final WindowState starting =
- createWindow(null, TYPE_APPLICATION_STARTING, app.mToken, "starting", uid);
+ final WindowState starting = newWindowBuilder("starting",
+ TYPE_APPLICATION_STARTING).setWindowToken(app.mToken).setOwnerId(uid).build();
starting.onSurfaceShownChanged(true);
assertFalse(mAtm.hasActiveVisibleWindow(uid));
@@ -1145,8 +1166,8 @@
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
@Test
public void testNeedsRelativeLayeringToIme_notAttached() {
- WindowState sameTokenWindow = createWindow(null, TYPE_BASE_APPLICATION, mAppWindow.mToken,
- "SameTokenWindow");
+ WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow",
+ TYPE_BASE_APPLICATION).setWindowToken(mAppWindow.mToken).build();
mDisplayContent.setImeLayeringTarget(mAppWindow);
makeWindowVisible(mImeWindow);
sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1158,8 +1179,8 @@
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD })
@Test
public void testNeedsRelativeLayeringToIme_startingWindow() {
- WindowState sameTokenWindow = createWindow(null, TYPE_APPLICATION_STARTING,
- mAppWindow.mToken, "SameTokenWindow");
+ WindowState sameTokenWindow = newWindowBuilder("SameTokenWindow",
+ TYPE_APPLICATION_STARTING).setWindowToken(mAppWindow.mToken).build();
mDisplayContent.setImeLayeringTarget(mAppWindow);
makeWindowVisible(mImeWindow);
sameTokenWindow.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1169,9 +1190,9 @@
@UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD})
@Test
public void testNeedsRelativeLayeringToIme_systemDialog() {
- WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
- mDisplayContent,
- "SystemDialog", true);
+ WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+ TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
mDisplayContent.setImeLayeringTarget(mAppWindow);
mAppWindow.getTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
makeWindowVisible(mImeWindow);
@@ -1182,20 +1203,21 @@
@UseTestDisplay(addWindows = {W_INPUT_METHOD})
@Test
public void testNeedsRelativeLayeringToIme_notificationShadeShouldNotHideSystemDialog() {
- WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
- mDisplayContent,
- "SystemDialog", true);
+ WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+ TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
mDisplayContent.setImeLayeringTarget(systemDialogWindow);
makeWindowVisible(mImeWindow);
- WindowState notificationShade = createWindow(null, TYPE_NOTIFICATION_SHADE,
- mDisplayContent, "NotificationShade", true);
+ WindowState notificationShade = newWindowBuilder("NotificationShade",
+ TYPE_NOTIFICATION_SHADE).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
notificationShade.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
assertFalse(notificationShade.needsRelativeLayeringToIme());
}
@Test
public void testSetFreezeInsetsState() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
spyOn(app);
doReturn(true).when(app).isVisible();
@@ -1216,7 +1238,7 @@
verify(app).notifyInsetsChanged();
// Verify that invisible non-activity window won't dispatch insets changed.
- final WindowState overlay = createWindow(null, TYPE_APPLICATION_OVERLAY, "overlay");
+ final WindowState overlay = newWindowBuilder("overlay", TYPE_APPLICATION_OVERLAY).build();
makeWindowVisible(overlay);
assertTrue(overlay.isReadyToDispatchInsetsState());
overlay.mHasSurface = false;
@@ -1244,9 +1266,9 @@
@Test
public void testAdjustImeInsetsVisibilityWhenSwitchingApps() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- final WindowState app2 = createWindow(null, TYPE_APPLICATION, "app2");
- final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
+ final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).build();
+ final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build();
spyOn(imeWindow);
doReturn(true).when(imeWindow).isVisible();
mDisplayContent.mInputMethodWindow = imeWindow;
@@ -1279,10 +1301,11 @@
@Test
public void testAdjustImeInsetsVisibilityWhenSwitchingApps_toAppInMultiWindowMode() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
- final WindowState app2 = createWindow(null, WINDOWING_MODE_MULTI_WINDOW,
- ACTIVITY_TYPE_STANDARD, TYPE_APPLICATION, mDisplayContent, "app2");
- final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
+ final WindowState app2 = newWindowBuilder("app2", TYPE_APPLICATION).setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+ mDisplayContent).build();
+ final WindowState imeWindow = newWindowBuilder("imeWindow", TYPE_APPLICATION).build();
spyOn(imeWindow);
doReturn(true).when(imeWindow).isVisible();
mDisplayContent.mInputMethodWindow = imeWindow;
@@ -1321,8 +1344,8 @@
@SetupWindows(addWindows = W_ACTIVITY)
@Test
public void testUpdateImeControlTargetWhenLeavingMultiWindow() {
- WindowState app = createWindow(null, TYPE_BASE_APPLICATION,
- mAppWindow.mToken, "app");
+ WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken(
+ mAppWindow.mToken).build();
mDisplayContent.setRemoteInsetsController(createDisplayWindowInsetsController());
spyOn(app);
@@ -1349,8 +1372,8 @@
@SetupWindows(addWindows = { W_ACTIVITY, W_INPUT_METHOD, W_NOTIFICATION_SHADE })
@Test
public void testNotificationShadeHasImeInsetsWhenMultiWindow() {
- WindowState app = createWindow(null, TYPE_BASE_APPLICATION,
- mAppWindow.mToken, "app");
+ WindowState app = newWindowBuilder("app", TYPE_BASE_APPLICATION).setWindowToken(
+ mAppWindow.mToken).build();
// Simulate entering multi-window mode and windowing mode is multi-window.
app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -1376,7 +1399,7 @@
@Test
public void testRequestedVisibility() {
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
app.mActivityRecord.setVisible(false);
app.mActivityRecord.setVisibility(false);
assertFalse(app.isVisibleRequested());
@@ -1391,7 +1414,7 @@
@Test
public void testKeepClearAreas() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
makeWindowVisible(window);
final Rect keepClearArea1 = new Rect(0, 0, 10, 10);
@@ -1433,7 +1456,7 @@
@Test
public void testUnrestrictedKeepClearAreas() {
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window");
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).build();
makeWindowVisible(window);
final Rect keepClearArea1 = new Rect(0, 0, 10, 10);
@@ -1481,8 +1504,9 @@
final InputMethodManagerInternal immi = InputMethodManagerInternal.get();
spyOn(immi);
- final WindowState imeTarget = createWindow(null /* parent */, TYPE_BASE_APPLICATION,
- createActivityRecord(mDisplayContent), "imeTarget");
+ final WindowState imeTarget = newWindowBuilder("imeTarget",
+ TYPE_BASE_APPLICATION).setWindowToken(
+ createActivityRecord(mDisplayContent)).build();
imeTarget.mActivityRecord.setVisibleRequested(true);
makeWindowVisible(imeTarget);
@@ -1562,8 +1586,8 @@
@Test
public void testIsSecureLocked_flagSecureSet() {
- WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
- 1 /* ownerId */);
+ WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+ 1).build();
window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE;
assertTrue(window.isSecureLocked());
@@ -1571,8 +1595,8 @@
@Test
public void testIsSecureLocked_flagSecureNotSet() {
- WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
- 1 /* ownerId */);
+ WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+ 1).build();
assertFalse(window.isSecureLocked());
}
@@ -1581,8 +1605,8 @@
public void testIsSecureLocked_disableSecureWindows() {
assumeTrue(Build.IS_DEBUGGABLE);
- WindowState window = createWindow(null /* parent */, TYPE_APPLICATION, "test-window",
- 1 /* ownerId */);
+ WindowState window = newWindowBuilder("test-window", TYPE_APPLICATION).setOwnerId(
+ 1).build();
window.mAttrs.flags |= WindowManager.LayoutParams.FLAG_SECURE;
ContentResolver cr = useFakeSettingsProvider();
@@ -1617,8 +1641,10 @@
String testPackage = "test";
int ownerId1 = 20;
int ownerId2 = 21;
- final WindowState window1 = createWindow(null, TYPE_APPLICATION, "window1", ownerId1);
- final WindowState window2 = createWindow(null, TYPE_APPLICATION, "window2", ownerId2);
+ final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setOwnerId(
+ ownerId1).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setOwnerId(
+ ownerId2).build();
// Setting packagename for targeted feature
window1.mAttrs.packageName = testPackage;
@@ -1638,7 +1664,8 @@
public void testIsSecureLocked_sensitiveContentBlockOrClearScreenCaptureForApp() {
String testPackage = "test";
int ownerId = 20;
- final WindowState window = createWindow(null, TYPE_APPLICATION, "window", ownerId);
+ final WindowState window = newWindowBuilder("window", TYPE_APPLICATION).setOwnerId(
+ ownerId).build();
window.mAttrs.packageName = testPackage;
assertFalse(window.isSecureLocked());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
index f226b9d..a02c3db 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTokenTests.java
@@ -74,11 +74,16 @@
assertEquals(0, token.getWindowsCount());
- final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
- final WindowState window11 = createWindow(window1, FIRST_SUB_WINDOW, token, "window11");
- final WindowState window12 = createWindow(window1, FIRST_SUB_WINDOW, token, "window12");
- final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
- final WindowState window3 = createWindow(null, TYPE_APPLICATION, token, "window3");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken(
+ token).build();
+ final WindowState window11 = newWindowBuilder("window11", FIRST_SUB_WINDOW).setParent(
+ window1).setWindowToken(token).build();
+ final WindowState window12 = newWindowBuilder("window12", FIRST_SUB_WINDOW).setParent(
+ window1).setWindowToken(token).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken(
+ token).build();
+ final WindowState window3 = newWindowBuilder("window3", TYPE_APPLICATION).setWindowToken(
+ token).build();
token.addWindow(window1);
// NOTE: Child windows will not be added to the token as window containers can only
@@ -105,8 +110,10 @@
public void testAddWindow_assignsLayers() {
final TestWindowToken token1 = createTestWindowToken(0, mDisplayContent);
final TestWindowToken token2 = createTestWindowToken(0, mDisplayContent);
- final WindowState window1 = createWindow(null, TYPE_STATUS_BAR, token1, "window1");
- final WindowState window2 = createWindow(null, TYPE_STATUS_BAR, token2, "window2");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_STATUS_BAR).setWindowToken(
+ token1).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_STATUS_BAR).setWindowToken(
+ token2).build();
token1.addWindow(window1);
token2.addWindow(window2);
@@ -122,8 +129,10 @@
assertEquals(token, dc.getWindowToken(token.token));
- final WindowState window1 = createWindow(null, TYPE_APPLICATION, token, "window1");
- final WindowState window2 = createWindow(null, TYPE_APPLICATION, token, "window2");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_APPLICATION).setWindowToken(
+ token).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_APPLICATION).setWindowToken(
+ token).build();
window2.removeImmediately();
// The token should still be mapped in the display content since it still has a child.
@@ -147,8 +156,10 @@
// Verify that the token is on the display
assertNotNull(mDisplayContent.getWindowToken(token.token));
- final WindowState window1 = createWindow(null, TYPE_TOAST, token, "window1");
- final WindowState window2 = createWindow(null, TYPE_TOAST, token, "window2");
+ final WindowState window1 = newWindowBuilder("window1", TYPE_TOAST).setWindowToken(
+ token).build();
+ final WindowState window2 = newWindowBuilder("window2", TYPE_TOAST).setWindowToken(
+ token).build();
mDisplayContent.removeWindowToken(token.token, true /* animateExit */);
// Verify that the token is no longer mapped on the display
@@ -231,7 +242,8 @@
assertNull(fromClientToken.mSurfaceControl);
- createWindow(null, TYPE_APPLICATION_OVERLAY, fromClientToken, "window");
+ newWindowBuilder("window", TYPE_APPLICATION_OVERLAY).setWindowToken(
+ fromClientToken).build();
assertNotNull(fromClientToken.mSurfaceControl);
final WindowToken nonClientToken = new WindowToken.Builder(mDisplayContent.mWmService,
@@ -285,7 +297,7 @@
// Simulate an app window to be the IME layering target, assume the app window has no
// frozen insets state by default.
- final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
+ final WindowState app = newWindowBuilder("app", TYPE_APPLICATION).build();
mDisplayContent.setImeLayeringTarget(app);
assertNull(app.getFrozenInsetsState());
assertTrue(app.isImeLayeringTarget());
@@ -299,7 +311,8 @@
@Test
public void testRemoveWindowToken_noAnimateExitWhenSet() {
final TestWindowToken token = createTestWindowToken(0, mDisplayContent);
- final WindowState win = createWindow(null, TYPE_APPLICATION, token, "win");
+ final WindowState win = newWindowBuilder("win", TYPE_APPLICATION).setWindowToken(
+ token).build();
makeWindowVisible(win);
assertTrue(win.isOnScreen());
spyOn(win);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 4f60106..84e2118 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -221,7 +221,7 @@
}
WindowState createWindow(String name) {
- return createWindow(null, TYPE_BASE_APPLICATION, mDisplayContent, name);
+ return newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay(mDisplayContent).build();
}
@Test
@@ -263,12 +263,12 @@
@Test
public void testAssignWindowLayers_ForImeWithAppTargetWithChildWindows() {
final WindowState imeAppTarget = createWindow("imeAppTarget");
- final WindowState imeAppTargetChildAboveWindow = createWindow(imeAppTarget,
- TYPE_APPLICATION_ATTACHED_DIALOG, imeAppTarget.mToken,
- "imeAppTargetChildAboveWindow");
- final WindowState imeAppTargetChildBelowWindow = createWindow(imeAppTarget,
- TYPE_APPLICATION_MEDIA_OVERLAY, imeAppTarget.mToken,
- "imeAppTargetChildBelowWindow");
+ final WindowState imeAppTargetChildAboveWindow = newWindowBuilder(
+ "imeAppTargetChildAboveWindow", TYPE_APPLICATION_ATTACHED_DIALOG).setParent(
+ imeAppTarget).setWindowToken(imeAppTarget.mToken).build();
+ final WindowState imeAppTargetChildBelowWindow = newWindowBuilder(
+ "imeAppTargetChildBelowWindow", TYPE_APPLICATION_MEDIA_OVERLAY).setParent(
+ imeAppTarget).setWindowToken(imeAppTarget.mToken).build();
mDisplayContent.setImeLayeringTarget(imeAppTarget);
makeWindowVisible(mImeWindow);
@@ -313,9 +313,9 @@
@Test
public void testAssignWindowLayers_ForImeNonAppImeTarget() {
- final WindowState imeSystemOverlayTarget = createWindow(null, TYPE_SYSTEM_OVERLAY,
- mDisplayContent, "imeSystemOverlayTarget",
- true /* ownerCanAddInternalSystemWindow */);
+ final WindowState imeSystemOverlayTarget = newWindowBuilder("imeSystemOverlayTarget",
+ TYPE_SYSTEM_OVERLAY).setDisplay(mDisplayContent).setOwnerCanAddInternalSystemWindow(
+ true).build();
mDisplayContent.setImeLayeringTarget(imeSystemOverlayTarget);
mDisplayContent.assignChildLayers(mTransaction);
@@ -354,18 +354,19 @@
@Test
public void testStackLayers() {
final WindowState anyWindow1 = createWindow("anyWindow");
- final WindowState pinnedStackWindow = createWindow(null, WINDOWING_MODE_PINNED,
- ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION, mDisplayContent,
- "pinnedStackWindow");
- final WindowState dockedStackWindow = createWindow(null,
- WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD, TYPE_BASE_APPLICATION,
- mDisplayContent, "dockedStackWindow");
- final WindowState assistantStackWindow = createWindow(null,
- WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_ASSISTANT, TYPE_BASE_APPLICATION,
- mDisplayContent, "assistantStackWindow");
- final WindowState homeActivityWindow = createWindow(null, WINDOWING_MODE_FULLSCREEN,
- ACTIVITY_TYPE_HOME, TYPE_BASE_APPLICATION,
- mDisplayContent, "homeActivityWindow");
+ final WindowState pinnedStackWindow = newWindowBuilder("pinnedStackWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_PINNED).setActivityType(
+ ACTIVITY_TYPE_STANDARD).setDisplay(mDisplayContent).build();
+ final WindowState dockedStackWindow = newWindowBuilder("dockedStackWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(
+ WINDOWING_MODE_MULTI_WINDOW).setActivityType(ACTIVITY_TYPE_STANDARD).setDisplay(
+ mDisplayContent).build();
+ final WindowState assistantStackWindow = newWindowBuilder("assistantStackWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType(
+ ACTIVITY_TYPE_ASSISTANT).setDisplay(mDisplayContent).build();
+ final WindowState homeActivityWindow = newWindowBuilder("homeActivityWindow",
+ TYPE_BASE_APPLICATION).setWindowingMode(WINDOWING_MODE_FULLSCREEN).setActivityType(
+ ACTIVITY_TYPE_HOME).setDisplay(mDisplayContent).build();
final WindowState anyWindow2 = createWindow("anyWindow2");
mDisplayContent.assignChildLayers(mTransaction);
@@ -383,13 +384,12 @@
@Test
public void testAssignWindowLayers_ForSysUiPanels() {
- final WindowState navBarPanel =
- createWindow(null, TYPE_NAVIGATION_BAR_PANEL, mDisplayContent, "NavBarPanel");
- final WindowState statusBarPanel =
- createWindow(null, TYPE_STATUS_BAR_ADDITIONAL, mDisplayContent,
- "StatusBarAdditional");
- final WindowState statusBarSubPanel =
- createWindow(null, TYPE_STATUS_BAR_SUB_PANEL, mDisplayContent, "StatusBarSubPanel");
+ final WindowState navBarPanel = newWindowBuilder("NavBarPanel",
+ TYPE_NAVIGATION_BAR_PANEL).setDisplay(mDisplayContent).build();
+ final WindowState statusBarPanel = newWindowBuilder("StatusBarAdditional",
+ TYPE_STATUS_BAR_ADDITIONAL).setDisplay(mDisplayContent).build();
+ final WindowState statusBarSubPanel = newWindowBuilder("StatusBarSubPanel",
+ TYPE_STATUS_BAR_SUB_PANEL).setDisplay(mDisplayContent).build();
mDisplayContent.assignChildLayers(mTransaction);
// Ime should be above all app windows and below system windows if it is targeting an app
@@ -401,15 +401,16 @@
@Test
public void testAssignWindowLayers_ForImeOnPopupImeLayeringTarget() {
- final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION,
- mAppWindow.mActivityRecord, "imeAppTarget");
+ final WindowState imeAppTarget = newWindowBuilder("imeAppTarget",
+ TYPE_APPLICATION).setWindowToken(mAppWindow.mActivityRecord).build();
mDisplayContent.setImeInputTarget(imeAppTarget);
mDisplayContent.setImeLayeringTarget(imeAppTarget);
mDisplayContent.setImeControlTarget(imeAppTarget);
// Set a popup IME layering target and keeps the original IME control target behinds it.
- final WindowState popupImeTargetWin = createWindow(imeAppTarget,
- TYPE_APPLICATION_SUB_PANEL, mAppWindow.mActivityRecord, "popupImeTargetWin");
+ final WindowState popupImeTargetWin = newWindowBuilder("popupImeTargetWin",
+ TYPE_APPLICATION_SUB_PANEL).setParent(imeAppTarget).setWindowToken(
+ mAppWindow.mActivityRecord).build();
mDisplayContent.setImeLayeringTarget(popupImeTargetWin);
mDisplayContent.updateImeParent();
@@ -424,11 +425,11 @@
// then we can drop all negative layering on the windowing side.
final WindowState anyWindow = createWindow("anyWindow");
- final WindowState child = createWindow(anyWindow, TYPE_APPLICATION_MEDIA, mDisplayContent,
- "TypeApplicationMediaChild");
- final WindowState mediaOverlayChild = createWindow(anyWindow,
- TYPE_APPLICATION_MEDIA_OVERLAY,
- mDisplayContent, "TypeApplicationMediaOverlayChild");
+ final WindowState child = newWindowBuilder("TypeApplicationMediaChild",
+ TYPE_APPLICATION_MEDIA).setParent(anyWindow).setDisplay(mDisplayContent).build();
+ final WindowState mediaOverlayChild = newWindowBuilder("TypeApplicationMediaOverlayChild",
+ TYPE_APPLICATION_MEDIA_OVERLAY).setParent(anyWindow).setDisplay(
+ mDisplayContent).build();
mDisplayContent.assignChildLayers(mTransaction);
@@ -440,14 +441,17 @@
public void testAssignWindowLayers_ForPostivelyZOrderedSubtype() {
final WindowState anyWindow = createWindow("anyWindow");
final ArrayList<WindowState> childList = new ArrayList<>();
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_PANEL, mDisplayContent,
- "TypeApplicationPanelChild"));
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_SUB_PANEL, mDisplayContent,
- "TypeApplicationSubPanelChild"));
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_ATTACHED_DIALOG, mDisplayContent,
- "TypeApplicationAttachedDialogChild"));
- childList.add(createWindow(anyWindow, TYPE_APPLICATION_ABOVE_SUB_PANEL, mDisplayContent,
- "TypeApplicationAboveSubPanelPanelChild"));
+ childList.add(newWindowBuilder("TypeApplicationPanelChild",
+ TYPE_APPLICATION_PANEL).setParent(anyWindow).setDisplay(mDisplayContent).build());
+ childList.add(newWindowBuilder("TypeApplicationSubPanelChild",
+ TYPE_APPLICATION_SUB_PANEL).setParent(anyWindow).setDisplay(
+ mDisplayContent).build());
+ childList.add(newWindowBuilder("TypeApplicationAttachedDialogChild",
+ TYPE_APPLICATION_ATTACHED_DIALOG).setParent(anyWindow).setDisplay(
+ mDisplayContent).build());
+ childList.add(newWindowBuilder("TypeApplicationAboveSubPanelPanelChild",
+ TYPE_APPLICATION_ABOVE_SUB_PANEL).setParent(anyWindow).setDisplay(
+ mDisplayContent).build());
final LayerRecordingTransaction t = mTransaction;
mDisplayContent.assignChildLayers(t);
@@ -469,8 +473,8 @@
// Create a popupWindow
assertWindowHigher(mImeWindow, mAppWindow);
- final WindowState popupWindow = createWindow(mAppWindow, TYPE_APPLICATION_PANEL,
- mDisplayContent, "PopupWindow");
+ final WindowState popupWindow = newWindowBuilder("PopupWindow",
+ TYPE_APPLICATION_PANEL).setParent(mAppWindow).setDisplay(mDisplayContent).build();
spyOn(popupWindow);
mDisplayContent.assignChildLayers(mTransaction);
@@ -492,8 +496,9 @@
makeWindowVisible(mImeWindow);
// Create a popupWindow
- final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
- mDisplayContent, "SystemDialog", true);
+ final WindowState systemDialogWindow = newWindowBuilder("SystemDialog",
+ TYPE_SECURE_SYSTEM_OVERLAY).setDisplay(
+ mDisplayContent).setOwnerCanAddInternalSystemWindow(true).build();
systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
spyOn(systemDialogWindow);
diff --git a/test-mock/Android.bp b/test-mock/Android.bp
index 71f3033..cadb0bd 100644
--- a/test-mock/Android.bp
+++ b/test-mock/Android.bp
@@ -72,6 +72,7 @@
"tests/**/*.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
// Make the current.txt available for use by the cts/tests/signature and /vendor tests.
diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
index df92898..9640a84 100644
--- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
+++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java
@@ -29,6 +29,7 @@
AppJankStats jankStats = new AppJankStats(
/*App Uid*/APP_ID,
/*Widget Id*/"test widget id",
+ /*navigationComponent*/null,
/*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL,
/*Widget State*/AppJankStats.WIDGET_STATE_SCROLLING,
/*Total Frames*/100,
diff --git a/tests/InputScreenshotTest/robotests/Android.bp b/tests/InputScreenshotTest/robotests/Android.bp
index b2414a8..63a1384 100644
--- a/tests/InputScreenshotTest/robotests/Android.bp
+++ b/tests/InputScreenshotTest/robotests/Android.bp
@@ -66,7 +66,6 @@
"android.test.mock.stubs.system",
"truth",
],
- upstream: true,
java_resource_dirs: ["config"],
instrumentation_for: "InputRoboApp",
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 9f35c7b..e294da1 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -65,6 +65,7 @@
"src/com/android/internal/util/ParcellingTests.java",
],
auto_gen_config: true,
+ team: "trendy_team_ravenwood",
}
java_test_helper_library {
diff --git a/tests/testables/tests/AndroidTest.xml b/tests/testables/tests/AndroidTest.xml
index 85f6e62..392bf67 100644
--- a/tests/testables/tests/AndroidTest.xml
+++ b/tests/testables/tests/AndroidTest.xml
@@ -45,6 +45,7 @@
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
<option name="directory-keys" value="/data/user/0/com.android.testables/files"/>
+ <option name="directory-keys" value="/data/user/10/com.android.testables/files"/>
<option name="collect-on-run-ended-only" value="true"/>
<option name="clean-up" value="true"/>
</metrics_collector>