Merge "If bubble bar is collapsing, hide the IME" into main
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
index 238c028..9eac108 100644
--- a/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
+++ b/apct-tests/perftests/core/src/android/libcore/regression/CipherPerfTest.java
@@ -18,6 +18,7 @@
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
import androidx.test.filters.LargeTest;
@@ -47,6 +48,8 @@
@RunWith(JUnitParamsRunner.class)
@LargeTest
public class CipherPerfTest {
+ private static final String TAG = "android.libcore.regression.CipherPerfTest";
+
@Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
public static Collection getCases() {
@@ -71,6 +74,10 @@
}
for (int keySize : keySizes) {
for (int inputSize : inputSizes) {
+ Log.i(TAG,
+ "param[" + params.size() + "] = " + mode.name() + ", "
+ + padding.name() + ", " + keySize + ", " + inputSize
+ + ", " + implementation.name());
params.add(
new Object[] {
mode, padding, keySize, inputSize, implementation
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index 52a761f..31d2ecd 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -34,6 +34,7 @@
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.os.Process;
import android.system.SystemCleaner;
import android.util.Log;
@@ -638,6 +639,12 @@
* @hide
*/
public void enableCleaner() {
+ // JobParameters objects are passed by reference in local Binder
+ // transactions for clients running as SYSTEM. The life cycle of the
+ // JobParameters objects are no longer controlled by the client.
+ if (Process.myUid() == Process.SYSTEM_UID) {
+ return;
+ }
if (mJobCleanupCallback == null) {
initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId));
}
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md
index 5d3f12e..6138388 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -83,6 +83,11 @@
Due to the sequential nature in which this is parsed, the `type` field must be specified before
the `data` field in this JSON Object.
+Every `register` command will need a `"UI_SET_EVBIT"` configuration entry that lists what types of
+axes it declares. This entry should be the first in the list. For example, if the uinput device has
+`"UI_SET_KEYBIT"` and `"UI_SET_RELBIT"` configuration entries, it will also need a `"UI_SET_EVBIT"`
+entry with data of `["EV_KEY", "EV_REL"]` or the other configuration entries will be ignored.
+
`ff_effects_max` must be provided if `UI_SET_FFBIT` is used in `configuration`.
`abs_info` fields are provided to set the device axes information. It is an array of below objects:
diff --git a/core/api/current.txt b/core/api/current.txt
index 67b3280..2bb08fc 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -6882,19 +6882,19 @@
public static final class Notification.ProgressStyle.Segment {
ctor public Notification.ProgressStyle.Segment(int);
method @ColorInt public int getColor();
+ method public int getId();
method public int getLength();
- method public int getStableId();
method @NonNull public android.app.Notification.ProgressStyle.Segment setColor(@ColorInt int);
- method @NonNull public android.app.Notification.ProgressStyle.Segment setStableId(int);
+ method @NonNull public android.app.Notification.ProgressStyle.Segment setId(int);
}
public static final class Notification.ProgressStyle.Step {
ctor public Notification.ProgressStyle.Step(int);
method @ColorInt public int getColor();
+ method public int getId();
method public int getPosition();
- method public int getStableId();
method @NonNull public android.app.Notification.ProgressStyle.Step setColor(@ColorInt int);
- method @NonNull public android.app.Notification.ProgressStyle.Step setStableId(int);
+ method @NonNull public android.app.Notification.ProgressStyle.Step setId(int);
}
public abstract static class Notification.Style {
@@ -8787,7 +8787,7 @@
@FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service {
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
- method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
+ method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>);
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
@@ -8822,13 +8822,12 @@
field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.ExecuteAppFunctionResponse> CREATOR;
field public static final String PROPERTY_RETURN_VALUE = "returnValue";
field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
- field public static final int RESULT_CANCELLED = 7; // 0x7
+ field public static final int RESULT_CANCELLED = 6; // 0x6
field public static final int RESULT_DENIED = 1; // 0x1
- field public static final int RESULT_DISABLED = 6; // 0x6
+ field public static final int RESULT_DISABLED = 5; // 0x5
field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
field public static final int RESULT_OK = 0; // 0x0
- field public static final int RESULT_TIMED_OUT = 5; // 0x5
}
}
@@ -12114,6 +12113,7 @@
method @NonNull public void setResourceValue(@NonNull String, int, @NonNull String, @Nullable String);
method @NonNull public void setResourceValue(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @Nullable String);
method @FlaggedApi("android.content.res.asset_file_descriptor_frro") @NonNull public void setResourceValue(@NonNull String, @NonNull android.content.res.AssetFileDescriptor, @Nullable String);
+ method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, int, @Nullable String);
method public void setTargetOverlayable(@Nullable String);
}
@@ -32821,7 +32821,7 @@
field @NonNull public static final String RELEASE_OR_PREVIEW_DISPLAY;
field @Deprecated public static final String SDK;
field public static final int SDK_INT;
- field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int SDK_MINOR_INT;
+ field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int SDK_INT_FULL;
field public static final String SECURITY_PATCH;
}
@@ -32865,6 +32865,9 @@
field public static final int VANILLA_ICE_CREAM = 35; // 0x23
}
+ @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static class Build.VERSION_CODES_FULL {
+ }
+
public final class Bundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable {
ctor public Bundle();
ctor public Bundle(ClassLoader);
@@ -54956,6 +54959,7 @@
method public boolean addAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener);
method public void addAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener, @Nullable android.os.Handler);
method public void addAudioDescriptionRequestedChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionRequestedChangeListener);
+ method @FlaggedApi("com.android.graphics.hwui.flags.high_contrast_text_small_text_rect") public void addHighContrastTextStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.HighContrastTextStateChangeListener);
method public boolean addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener);
method public void addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener, @Nullable android.os.Handler);
method @ColorInt public int getAccessibilityFocusColor();
@@ -54968,12 +54972,14 @@
method public static boolean isAccessibilityButtonSupported();
method public boolean isAudioDescriptionRequested();
method public boolean isEnabled();
+ method @FlaggedApi("com.android.graphics.hwui.flags.high_contrast_text_small_text_rect") public boolean isHighContrastTextEnabled();
method public boolean isRequestFromAccessibilityTool();
method public boolean isTouchExplorationEnabled();
method public void removeAccessibilityRequestPreparer(android.view.accessibility.AccessibilityRequestPreparer);
method public boolean removeAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener);
method public boolean removeAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener);
method public boolean removeAudioDescriptionRequestedChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionRequestedChangeListener);
+ method @FlaggedApi("com.android.graphics.hwui.flags.high_contrast_text_small_text_rect") public void removeHighContrastTextStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.HighContrastTextStateChangeListener);
method public boolean removeTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener);
method public void sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
field public static final int FLAG_CONTENT_CONTROLS = 4; // 0x4
@@ -54993,6 +54999,10 @@
method public void onAudioDescriptionRequestedChanged(boolean);
}
+ @FlaggedApi("com.android.graphics.hwui.flags.high_contrast_text_small_text_rect") public static interface AccessibilityManager.HighContrastTextStateChangeListener {
+ method public void onHighContrastTextStateChanged(boolean);
+ }
+
public static interface AccessibilityManager.TouchExplorationStateChangeListener {
method public void onTouchExplorationStateChanged(boolean);
}
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 8447a7f..287e787 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -604,6 +604,7 @@
public class TelephonyManager {
method @NonNull public static int[] getAllNetworkTypes();
+ method @FlaggedApi("android.os.mainline_vcn_platform_api") @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Set<java.lang.String> getPackagesWithCarrierPrivileges();
}
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 20bcf5f..4b6c62e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -91,6 +91,7 @@
field public static final String BIND_TRANSLATION_SERVICE = "android.permission.BIND_TRANSLATION_SERVICE";
field public static final String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
field public static final String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String BIND_VERIFICATION_AGENT = "android.permission.BIND_VERIFICATION_AGENT";
field public static final String BIND_VISUAL_QUERY_DETECTION_SERVICE = "android.permission.BIND_VISUAL_QUERY_DETECTION_SERVICE";
field public static final String BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE = "android.permission.BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE";
field public static final String BIND_WEARABLE_SENSING_SERVICE = "android.permission.BIND_WEARABLE_SENSING_SERVICE";
@@ -412,6 +413,7 @@
field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String USE_ON_DEVICE_INTELLIGENCE = "android.permission.USE_ON_DEVICE_INTELLIGENCE";
field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK";
field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String VERIFICATION_AGENT = "android.permission.VERIFICATION_AGENT";
field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String VIBRATE_VENDOR_EFFECTS = "android.permission.VIBRATE_VENDOR_EFFECTS";
field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS";
field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS";
@@ -4303,6 +4305,7 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>);
field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
+ field @FlaggedApi("android.content.pm.verification_service") public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE";
field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_APK = 1; // 0x1
field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_INSTALLER = 2; // 0x2
field @FlaggedApi("android.content.pm.asl_in_apk_app_metadata_source") public static final int APP_METADATA_SOURCE_SYSTEM_IMAGE = 3; // 0x3
@@ -4616,6 +4619,61 @@
}
+package android.content.pm.verify.pkg {
+
+ @FlaggedApi("android.content.pm.verification_service") public final class VerificationSession implements android.os.Parcelable {
+ method public int describeContents();
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long extendTimeRemaining(long);
+ method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getDeclaredLibraries();
+ method @NonNull public android.os.PersistableBundle getExtensionParams();
+ method public int getId();
+ method public int getInstallSessionId();
+ method @NonNull public String getPackageName();
+ method @NonNull public android.content.pm.SigningInfo getSigningInfo();
+ method @NonNull public android.net.Uri getStagedPackageUri();
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime();
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus);
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle);
+ method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int);
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR;
+ field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2
+ field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1
+ field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0
+ }
+
+ @FlaggedApi("android.content.pm.verification_service") public final class VerificationStatus implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAslStatus();
+ method @NonNull public String getFailureMessage();
+ method public boolean isVerified();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationStatus> CREATOR;
+ field public static final int VERIFIER_STATUS_ASL_BAD = 2; // 0x2
+ field public static final int VERIFIER_STATUS_ASL_GOOD = 1; // 0x1
+ field public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0; // 0x0
+ }
+
+ public static final class VerificationStatus.Builder {
+ ctor public VerificationStatus.Builder();
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus build();
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setAslStatus(int);
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setFailureMessage(@NonNull String);
+ method @NonNull public android.content.pm.verify.pkg.VerificationStatus.Builder setVerified(boolean);
+ }
+
+ @FlaggedApi("android.content.pm.verification_service") public abstract class VerifierService extends android.app.Service {
+ ctor public VerifierService();
+ method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
+ method public abstract void onPackageNameAvailable(@NonNull String);
+ method public abstract void onVerificationCancelled(@NonNull String);
+ method public abstract void onVerificationRequired(@NonNull android.content.pm.verify.pkg.VerificationSession);
+ method public abstract void onVerificationRetry(@NonNull android.content.pm.verify.pkg.VerificationSession);
+ method public abstract void onVerificationTimeout(int);
+ }
+
+}
+
package android.content.rollback {
public final class PackageRollbackInfo implements android.os.Parcelable {
@@ -13046,6 +13104,7 @@
method public void onPanelHidden();
method public void onPanelRevealed(int);
method public void onSuggestedReplySent(@NonNull String, @NonNull CharSequence, int);
+ method @FlaggedApi("android.service.notification.notification_classification") public final void setAdjustmentTypeSupportedState(@NonNull String, boolean);
method public final void unsnoozeNotification(@NonNull String);
field public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
field @FlaggedApi("android.service.notification.notification_classification") public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index f32d805..9bcdf95 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -396,6 +396,7 @@
method public void cleanUpCallersAfter(long);
method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
method public android.content.ComponentName getEffectsSuppressor();
+ method @FlaggedApi("android.service.notification.notification_classification") @NonNull public java.util.Set<java.lang.String> getUnsupportedAdjustmentTypes();
method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean);
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index edcdb6c..f34341f 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -34,6 +34,7 @@
import android.util.proto.ProtoOutputStream;
import android.util.proto.WireTypeMismatchException;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -777,7 +778,9 @@
mStartComponent = other.mStartComponent;
}
- private ApplicationStartInfo(@NonNull Parcel in) {
+ /** @hide */
+ @VisibleForTesting
+ public ApplicationStartInfo(@NonNull Parcel in) {
mStartupState = in.readInt();
mPid = in.readInt();
mRealUid = in.readInt();
@@ -1061,12 +1064,21 @@
if (other == null || !(other instanceof ApplicationStartInfo)) {
return false;
}
+
final ApplicationStartInfo o = (ApplicationStartInfo) other;
- return mPid == o.mPid && mRealUid == o.mRealUid && mPackageUid == o.mPackageUid
- && mDefiningUid == o.mDefiningUid && mReason == o.mReason
- && mStartupState == o.mStartupState && mStartType == o.mStartType
- && mLaunchMode == o.mLaunchMode && TextUtils.equals(mProcessName, o.mProcessName)
- && timestampsEquals(o) && mWasForceStopped == o.mWasForceStopped
+
+ return mPid == o.mPid
+ && mRealUid == o.mRealUid
+ && mPackageUid == o.mPackageUid
+ && mDefiningUid == o.mDefiningUid
+ && mReason == o.mReason
+ && mStartupState == o.mStartupState
+ && mStartType == o.mStartType
+ && mLaunchMode == o.mLaunchMode
+ && TextUtils.equals(mPackageName, o.mPackageName)
+ && TextUtils.equals(mProcessName, o.mProcessName)
+ && timestampsEquals(o)
+ && mWasForceStopped == o.mWasForceStopped
&& mMonoticCreationTimeMs == o.mMonoticCreationTimeMs
&& mStartComponent == o.mStartComponent;
}
@@ -1074,7 +1086,7 @@
@Override
public int hashCode() {
return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState,
- mStartType, mLaunchMode, mProcessName, mStartupTimestampsNs,
+ mStartType, mLaunchMode, mPackageName, mProcessName, mStartupTimestampsNs,
mMonoticCreationTimeMs, mStartComponent);
}
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 8a54b5d..3b2aab4 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -160,8 +160,8 @@
void requestBindProvider(in ComponentName component);
void requestUnbindProvider(in IConditionProvider token);
- void setNotificationsShownFromListener(in INotificationListener token, in String[] keys);
+ void setNotificationsShownFromListener(in INotificationListener token, in String[] keys);
ParceledListSlice getActiveNotificationsFromListener(in INotificationListener token, in String[] keys, int trim);
ParceledListSlice getSnoozedNotificationsFromListener(in INotificationListener token, int trim);
void clearRequestedListenerHints(in INotificationListener token);
@@ -261,4 +261,7 @@
void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser);
boolean appCanBePromoted(String pkg, int uid);
boolean canBePromoted(String pkg);
+
+ void setAdjustmentTypeSupportedState(in INotificationListener token, String key, boolean supported);
+ List<String> getUnsupportedAdjustmentTypes();
}
diff --git a/core/java/android/app/IUserSwitchObserver.aidl b/core/java/android/app/IUserSwitchObserver.aidl
index cfdb426..1ff7a17 100644
--- a/core/java/android/app/IUserSwitchObserver.aidl
+++ b/core/java/android/app/IUserSwitchObserver.aidl
@@ -19,10 +19,10 @@
import android.os.IRemoteCallback;
/** {@hide} */
-oneway interface IUserSwitchObserver {
+interface IUserSwitchObserver {
void onBeforeUserSwitching(int newUserId);
- void onUserSwitching(int newUserId, IRemoteCallback reply);
- void onUserSwitchComplete(int newUserId);
- void onForegroundProfileSwitch(int newProfileId);
- void onLockedBootComplete(int newUserId);
+ oneway void onUserSwitching(int newUserId, IRemoteCallback reply);
+ oneway void onUserSwitchComplete(int newUserId);
+ oneway void onForegroundProfileSwitch(int newProfileId);
+ oneway void onLockedBootComplete(int newUserId);
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index e8b0a36f..34d0f3b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -11199,7 +11199,7 @@
*/
@FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
public static class ProgressStyle extends Notification.Style {
- private static final String KEY_ELEMENT_STABLE_ID = "stableId";
+ private static final String KEY_ELEMENT_ID = "id";
private static final String KEY_ELEMENT_COLOR = "colorInt";
private static final String KEY_SEGMENT_LENGTH = "length";
private static final String KEY_STEP_POSITION = "position";
@@ -11626,7 +11626,7 @@
final Bundle bundle = new Bundle();
bundle.putInt(KEY_SEGMENT_LENGTH, segment.getLength());
- bundle.putInt(KEY_ELEMENT_STABLE_ID, segment.getStableId());
+ bundle.putInt(KEY_ELEMENT_ID, segment.getId());
bundle.putInt(KEY_ELEMENT_COLOR, segment.getColor());
segments.add(bundle);
@@ -11647,11 +11647,11 @@
continue;
}
- final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+ final int id = segmentBundle.getInt(KEY_ELEMENT_ID);
final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
Notification.COLOR_DEFAULT);
final Segment segment = new Segment(length)
- .setStableId(stableId).setColor(color);
+ .setId(id).setColor(color);
segments.add(segment);
}
@@ -11672,7 +11672,7 @@
final Bundle bundle = new Bundle();
bundle.putInt(KEY_STEP_POSITION, step.getPosition());
- bundle.putInt(KEY_ELEMENT_STABLE_ID, step.getStableId());
+ bundle.putInt(KEY_ELEMENT_ID, step.getId());
bundle.putInt(KEY_ELEMENT_COLOR, step.getColor());
steps.add(bundle);
@@ -11693,10 +11693,10 @@
if (position < 0) {
continue;
}
- final int stableId = segmentBundle.getInt(KEY_ELEMENT_STABLE_ID);
+ final int id = segmentBundle.getInt(KEY_ELEMENT_ID);
final int color = segmentBundle.getInt(KEY_ELEMENT_COLOR,
Notification.COLOR_DEFAULT);
- final Step step = new Step(position).setStableId(stableId).setColor(color);
+ final Step step = new Step(position).setId(id).setColor(color);
steps.add(step);
}
}
@@ -11712,7 +11712,7 @@
*/
public static final class Segment {
private int mLength;
- private int mStableId = 0;
+ private int mId = 0;
@ColorInt
private int mColor = Notification.COLOR_DEFAULT;
@@ -11735,19 +11735,19 @@
}
/**
- * Gets the stable id of this Segment.
+ * Gets the id of this Segment.
*
- * @see #setStableId
+ * @see #setId
*/
- public int getStableId() {
- return mStableId;
+ public int getId() {
+ return mId;
}
/**
* Optional ID used to uniquely identify the element across updates.
*/
- public @NonNull Segment setStableId(int stableId) {
- mStableId = stableId;
+ public @NonNull Segment setId(int id) {
+ mId = id;
return this;
}
@@ -11777,13 +11777,13 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Segment segment = (Segment) o;
- return mLength == segment.mLength && mStableId == segment.mStableId
+ return mLength == segment.mLength && mId == segment.mId
&& mColor == segment.mColor;
}
@Override
public int hashCode() {
- return Objects.hash(mLength, mStableId, mColor);
+ return Objects.hash(mLength, mId, mColor);
}
}
@@ -11797,7 +11797,7 @@
public static final class Step {
private int mPosition;
- private int mStableId;
+ private int mId;
@ColorInt
private int mColor = Notification.COLOR_DEFAULT;
@@ -11823,17 +11823,17 @@
/**
- * Optional ID used to uniqurely identify the element across updates.
+ * Optional ID used to uniquely identify the element across updates.
*/
- public int getStableId() {
- return mStableId;
+ public int getId() {
+ return mId;
}
/**
- * Optional ID used to uniqurely identify the element across updates.
+ * Optional ID used to uniquely identify the element across updates.
*/
- public @NonNull Step setStableId(int stableId) {
- mStableId = stableId;
+ public @NonNull Step setId(int id) {
+ mId = id;
return this;
}
@@ -11863,13 +11863,13 @@
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Step step = (Step) o;
- return mPosition == step.mPosition && mStableId == step.mStableId
+ return mPosition == step.mPosition && mId == step.mId
&& mColor == step.mColor;
}
@Override
public int hashCode() {
- return Objects.hash(mPosition, mStableId, mColor);
+ return Objects.hash(mPosition, mId, mColor);
}
}
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index c7b84ae..dfed1f7 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -68,9 +68,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
/**
@@ -3094,4 +3096,19 @@
}
}
+ /**
+ * Returns the list of {@link Adjustment} keys that the current approved
+ * {@link android.service.notification.NotificationAssistantService} does not support.
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public @NonNull Set<String> getUnsupportedAdjustmentTypes() {
+ INotificationManager service = getService();
+ try {
+ return new HashSet<>(service.getUnsupportedAdjustmentTypes());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java
index 8e41773..7a68a65 100644
--- a/core/java/android/app/appfunctions/AppFunctionService.java
+++ b/core/java/android/app/appfunctions/AppFunctionService.java
@@ -35,6 +35,7 @@
import android.os.CancellationSignal;
import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.util.Log;
import java.util.function.Consumer;
@@ -166,9 +167,13 @@
*/
@MainThread
@Deprecated
- public abstract void onExecuteFunction(
+ public void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ Log.w(
+ "AppFunctionService",
+ "Calling deprecated default implementation of onExecuteFunction");
+ }
/**
* Called by the system to execute a specific app function.
diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
index 2851e92..a879b1b 100644
--- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
+++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java
@@ -96,17 +96,14 @@
*/
public static final int RESULT_INVALID_ARGUMENT = 4;
- /** The operation was timed out. */
- public static final int RESULT_TIMED_OUT = 5;
-
/** The caller tried to execute a disabled app function. */
- public static final int RESULT_DISABLED = 6;
+ public static final int RESULT_DISABLED = 5;
/**
* The operation was cancelled. Use this error code to report that a cancellation is done after
* receiving a cancellation signal.
*/
- public static final int RESULT_CANCELLED = 7;
+ public static final int RESULT_CANCELLED = 6;
/** The result code of the app function execution. */
@ResultCode private final int mResultCode;
@@ -282,7 +279,6 @@
RESULT_APP_UNKNOWN_ERROR,
RESULT_INTERNAL_ERROR,
RESULT_INVALID_ARGUMENT,
- RESULT_TIMED_OUT,
RESULT_DISABLED,
RESULT_CANCELLED
})
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 8916ce2..6fe0a73 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -84,11 +84,16 @@
int getDevicePolicy(int policyType);
/**
- * Returns whether the device has a valid microphone.
- */
+ * Returns whether the device has a valid microphone.
+ */
boolean hasCustomAudioInputSupport();
/**
+ * Returns whether this device is allowed to create mirror displays.
+ */
+ boolean canCreateMirrorDisplays();
+
+ /**
* Closes the virtual device and frees all associated resources.
*/
@EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index e9fa3e1..9eb6d56 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -132,8 +132,16 @@
}
flag {
- namespace: "virtual_devices"
- name: "camera_timestamp_from_surface"
- description: "Pass the surface timestamp to the capture result"
- bug: "351341245"
+ namespace: "virtual_devices"
+ name: "camera_timestamp_from_surface"
+ description: "Pass the surface timestamp to the capture result"
+ bug: "351341245"
+}
+
+flag {
+ namespace: "virtual_devices"
+ name: "enable_limited_vdm_role"
+ description: "New VDM role without trusted displays or input"
+ bug: "370657575"
+ is_exported: true
}
diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java
index 40ffb0f..64e9c33 100644
--- a/core/java/android/content/om/FabricatedOverlay.java
+++ b/core/java/android/content/om/FabricatedOverlay.java
@@ -476,6 +476,20 @@
return entry;
}
+ @NonNull
+ private static FabricatedOverlayInternalEntry generateFabricatedOverlayInternalEntry(
+ @NonNull String resourceName, float dimensionValue,
+ @TypedValue.ComplexDimensionUnit int dimensionUnit, @Nullable String configuration) {
+ final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
+ entry.resourceName = resourceName;
+ entry.dataType = TypedValue.TYPE_DIMENSION;
+ Preconditions.checkArgumentInRange(dimensionUnit,
+ TypedValue.COMPLEX_UNIT_PX, TypedValue.COMPLEX_UNIT_MM, "dimensionUnit");
+ entry.data = TypedValue.createComplexDimension(dimensionValue, dimensionUnit);
+ entry.configuration = configuration;
+ return entry;
+ }
+
/**
* Sets the resource value in the fabricated overlay for the integer-like types with the
* configuration.
@@ -586,4 +600,25 @@
mOverlay.entries.add(
generateFabricatedOverlayInternalEntry(resourceName, value, configuration));
}
+
+ /**
+ * Sets the resource value in the fabricated overlay for the dimension type with the
+ * configuration.
+ *
+ * @param resourceName name of the target resource to overlay (in the form
+ * [package]:type/entry)
+ * @param dimensionValue the float representing the dimension value
+ * @param dimensionUnit the integer representing the dimension unit
+ * @param configuration The string representation of the config this overlay is enabled for
+ */
+ @FlaggedApi(android.content.res.Flags.FLAG_DIMENSION_FRRO)
+ public void setResourceValue(
+ @NonNull String resourceName,
+ float dimensionValue,
+ @TypedValue.ComplexDimensionUnit int dimensionUnit,
+ @Nullable String configuration) {
+ ensureValidResourceName(resourceName);
+ mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, dimensionValue,
+ dimensionUnit, configuration));
+ }
}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index fb2655c..e985f88 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -5039,6 +5039,25 @@
"android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
/**
+ * Used by the system to query a {@link android.content.pm.verify.pkg.VerifierService} provider,
+ * which registers itself via an intent-filter handling this action.
+ *
+ * <p class="note">Only the system can bind to such a verifier service. This is protected by the
+ * {@link android.Manifest.permission#BIND_VERIFICATION_AGENT} permission. The verifier service
+ * app should protect the service by adding this permission in the service declaration in its
+ * manifest.
+ * <p>
+ * A verifier service must be a privileged app and hold the
+ * {@link android.Manifest.permission#VERIFICATION_AGENT} permission.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(android.content.pm.Flags.FLAG_VERIFICATION_SERVICE)
+ @SdkConstant(SdkConstantType.SERVICE_ACTION)
+ public static final String ACTION_VERIFY_PACKAGE = "android.content.pm.action.VERIFY_PACKAGE";
+
+ /**
* The names of the requested permissions.
* <p>
* <strong>Type:</strong> String[]
diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING
index 2cdae21..44f2a4c 100644
--- a/core/java/android/content/pm/TEST_MAPPING
+++ b/core/java/android/content/pm/TEST_MAPPING
@@ -114,6 +114,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"options":[
{
@@ -125,6 +136,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"options":[
{
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 160cbdf..300740e 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -309,3 +309,11 @@
description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently."
bug: "361776825"
}
+
+flag {
+ name: "verification_service"
+ namespace: "package_manager_service"
+ description: "Feature flag to enable the new verification service."
+ bug: "360129103"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index fd1a896..d5edc92 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -68,6 +68,13 @@
}
flag {
+ name: "multiuser_widget"
+ namespace: "multiuser"
+ description: "Implement the Multiuser Widget"
+ bug: "365748524"
+}
+
+flag {
name: "enable_biometrics_to_unlock_private_space"
is_exported: true
namespace: "profile_experiences"
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl
new file mode 100644
index 0000000..38a7956
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.os.PersistableBundle;
+
+/**
+ * Oneway interface that allows the verifier to send response or verification results back to
+ * the system.
+ * @hide
+ */
+oneway interface IVerificationSessionCallback {
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ void reportVerificationIncomplete(int verificationId, int reason);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ void reportVerificationComplete(int verificationId, in VerificationStatus status);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ void reportVerificationCompleteWithExtensionResponse(int verificationId, in VerificationStatus status, in PersistableBundle response);
+}
diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
new file mode 100644
index 0000000..7a9484a
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+/**
+ * Non-oneway interface that allows the verifier to retrieve information from the system.
+ * @hide
+ */
+interface IVerificationSessionInterface {
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ long getTimeoutTime(int verificationId);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)")
+ long extendTimeRemaining(int verificationId, long additionalMs);
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/IVerifierService.aidl b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl
new file mode 100644
index 0000000..d3071fd
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/IVerifierService.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.content.pm.verify.pkg.VerificationSession;
+
+/**
+ * Oneway interface that allows the system to communicate to the verifier service agent.
+ * @hide
+ */
+oneway interface IVerifierService {
+ void onPackageNameAvailable(in String packageName);
+ void onVerificationCancelled(in String packageName);
+ void onVerificationRequired(in VerificationSession session);
+ void onVerificationRetry(in VerificationSession session);
+ void onVerificationTimeout(int verificationId);
+}
\ No newline at end of file
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.aidl b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl
new file mode 100644
index 0000000..ac85585
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+/** @hide */
+parcelable VerificationSession;
diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java
new file mode 100644
index 0000000..70b4a02
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.pm.Flags;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class is used by the system to describe the details about a verification request sent to the
+ * verification agent, aka the verifier. It includes the interfaces for the verifier to communicate
+ * back to the system.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+@SystemApi
+public final class VerificationSession implements Parcelable {
+ /**
+ * The verification cannot be completed because of unknown reasons.
+ */
+ public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0;
+ /**
+ * The verification cannot be completed because the network is unavailable.
+ */
+ public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1;
+ /**
+ * The verification cannot be completed because the network is limited.
+ */
+ public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = {
+ VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE,
+ VERIFICATION_INCOMPLETE_NETWORK_LIMITED,
+ VERIFICATION_INCOMPLETE_UNKNOWN,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VerificationIncompleteReason {
+ }
+
+ private final int mId;
+ private final int mInstallSessionId;
+ @NonNull
+ private final String mPackageName;
+ @NonNull
+ private final Uri mStagedPackageUri;
+ @NonNull
+ private final SigningInfo mSigningInfo;
+ @NonNull
+ private final List<SharedLibraryInfo> mDeclaredLibraries;
+ @NonNull
+ private final PersistableBundle mExtensionParams;
+ @NonNull
+ private final IVerificationSessionInterface mSession;
+ @NonNull
+ private final IVerificationSessionCallback mCallback;
+
+ /**
+ * Constructor used by the system to describe the details of a verification session.
+ * @hide
+ */
+ public VerificationSession(int id, int installSessionId, @NonNull String packageName,
+ @NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo,
+ @NonNull List<SharedLibraryInfo> declaredLibraries,
+ @NonNull PersistableBundle extensionParams,
+ @NonNull IVerificationSessionInterface session,
+ @NonNull IVerificationSessionCallback callback) {
+ mId = id;
+ mInstallSessionId = installSessionId;
+ mPackageName = packageName;
+ mStagedPackageUri = stagedPackageUri;
+ mSigningInfo = signingInfo;
+ mDeclaredLibraries = declaredLibraries;
+ mExtensionParams = extensionParams;
+ mSession = session;
+ mCallback = callback;
+ }
+
+ /**
+ * A unique identifier tied to this specific verification session.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * The package name of the app that is to be verified.
+ */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * The id of the installation session associated with the verification.
+ */
+ public int getInstallSessionId() {
+ return mInstallSessionId;
+ }
+
+ /**
+ * The Uri of the path where the package's code files are located.
+ */
+ public @NonNull Uri getStagedPackageUri() {
+ return mStagedPackageUri;
+ }
+
+ /**
+ * Signing info of the package to be verified.
+ */
+ public @NonNull SigningInfo getSigningInfo() {
+ return mSigningInfo;
+ }
+
+ /**
+ * Returns a mapping of any shared libraries declared in the manifest
+ * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty
+ * map if no shared libraries are declared by the package.
+ */
+ @NonNull
+ public List<SharedLibraryInfo> getDeclaredLibraries() {
+ return Collections.unmodifiableList(mDeclaredLibraries);
+ }
+
+ /**
+ * Returns any extension params associated with the verification request.
+ */
+ @NonNull
+ public PersistableBundle getExtensionParams() {
+ return mExtensionParams;
+ }
+
+ /**
+ * Get the value of Clock.elapsedRealtime() at which time this verification
+ * will timeout as incomplete if no other verification response is provided.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public long getTimeoutTime() {
+ try {
+ return mSession.getTimeoutTime(mId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Extend the timeout for this session by the provided additionalMs to
+ * fetch relevant information over the network or wait for the network.
+ * This may be called multiple times. If the request would bypass any max
+ * duration by the system, the method will return a lower value than the
+ * requested amount that indicates how much the time was extended.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public long extendTimeRemaining(long additionalMs) {
+ try {
+ return mSession.extendTimeRemaining(mId, additionalMs);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Report to the system that verification could not be completed along
+ * with an approximate reason to pass on to the installer.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public void reportVerificationIncomplete(@VerificationIncompleteReason int reason) {
+ try {
+ mCallback.reportVerificationIncomplete(mId, reason);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Report to the system that the verification has completed and the
+ * install process may act on that status to either block in the case
+ * of failure or continue to process the install in the case of success.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public void reportVerificationComplete(@NonNull VerificationStatus status) {
+ try {
+ mCallback.reportVerificationComplete(mId, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Same as {@link #reportVerificationComplete(VerificationStatus)}, but also provide
+ * a result to the extension params provided in the request, which will be passed to the
+ * installer in the installation result.
+ */
+ @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)
+ public void reportVerificationComplete(@NonNull VerificationStatus status,
+ @NonNull PersistableBundle response) {
+ try {
+ mCallback.reportVerificationCompleteWithExtensionResponse(mId, status, response);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ private VerificationSession(@NonNull Parcel in) {
+ mId = in.readInt();
+ mInstallSessionId = in.readInt();
+ mPackageName = in.readString8();
+ mStagedPackageUri = Uri.CREATOR.createFromParcel(in);
+ mSigningInfo = SigningInfo.CREATOR.createFromParcel(in);
+ mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR);
+ mExtensionParams = in.readPersistableBundle(getClass().getClassLoader());
+ mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder());
+ mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeInt(mInstallSessionId);
+ dest.writeString8(mPackageName);
+ Uri.writeToParcel(dest, mStagedPackageUri);
+ mSigningInfo.writeToParcel(dest, flags);
+ dest.writeTypedList(mDeclaredLibraries);
+ dest.writePersistableBundle(mExtensionParams);
+ dest.writeStrongBinder(mSession.asBinder());
+ dest.writeStrongBinder(mCallback.asBinder());
+ }
+
+ @NonNull
+ public static final Creator<VerificationSession> CREATOR = new Creator<>() {
+ @Override
+ public VerificationSession createFromParcel(@NonNull Parcel in) {
+ return new VerificationSession(in);
+ }
+
+ @Override
+ public VerificationSession[] newArray(int size) {
+ return new VerificationSession[size];
+ }
+ };
+}
diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl
new file mode 100644
index 0000000..6a1cb4f
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+/** @hide */
+parcelable VerificationStatus;
diff --git a/core/java/android/content/pm/verify/pkg/VerificationStatus.java b/core/java/android/content/pm/verify/pkg/VerificationStatus.java
new file mode 100644
index 0000000..4d0379d7
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerificationStatus.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.content.pm.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class is used by the verifier to describe the status of the verification request, whether
+ * it's successful or it has failed along with any relevant details.
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+public final class VerificationStatus implements Parcelable {
+ /**
+ * The ASL status has not been determined. This happens in situations where the verification
+ * service is not monitoring ASLs, and means the ASL data in the app is not necessarily bad but
+ * can't be trusted.
+ */
+ public static final int VERIFIER_STATUS_ASL_UNDEFINED = 0;
+
+ /**
+ * The app's ASL data is considered to be in a good state.
+ */
+ public static final int VERIFIER_STATUS_ASL_GOOD = 1;
+
+ /**
+ * There is something bad in the app's ASL data; the user should be warned about this when shown
+ * the ASL data and/or appropriate decisions made about the use of this data by the platform.
+ */
+ public static final int VERIFIER_STATUS_ASL_BAD = 2;
+
+ /** @hide */
+ @IntDef(prefix = {"VERIFIER_STATUS_ASL_"}, value = {
+ VERIFIER_STATUS_ASL_UNDEFINED,
+ VERIFIER_STATUS_ASL_GOOD,
+ VERIFIER_STATUS_ASL_BAD,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface VerifierStatusAsl {}
+
+ private boolean mIsVerified;
+ private @VerifierStatusAsl int mAslStatus;
+ @NonNull
+ private String mFailuresMessage = "";
+
+ private VerificationStatus() {}
+
+ /**
+ * @return whether the status is set to verified or not.
+ */
+ public boolean isVerified() {
+ return mIsVerified;
+ }
+
+ /**
+ * @return the failure message associated with the failure status.
+ */
+ @NonNull
+ public String getFailureMessage() {
+ return mFailuresMessage;
+ }
+
+ /**
+ * @return the asl status.
+ */
+ public @VerifierStatusAsl int getAslStatus() {
+ return mAslStatus;
+ }
+
+ /**
+ * Builder to construct a {@link VerificationStatus} object.
+ */
+ public static final class Builder {
+ final VerificationStatus mStatus = new VerificationStatus();
+
+ /**
+ * Set in the status whether the verification has succeeded or failed.
+ */
+ @NonNull
+ public Builder setVerified(boolean verified) {
+ mStatus.mIsVerified = verified;
+ return this;
+ }
+
+ /**
+ * Set a developer-facing failure message to include in the verification failure status.
+ */
+ @NonNull
+ public Builder setFailureMessage(@NonNull String failureMessage) {
+ mStatus.mFailuresMessage = failureMessage;
+ return this;
+ }
+
+ /**
+ * Set the ASL status, as defined in {@link VerifierStatusAsl}.
+ */
+ @NonNull
+ public Builder setAslStatus(@VerifierStatusAsl int aslStatus) {
+ mStatus.mAslStatus = aslStatus;
+ return this;
+ }
+
+ /**
+ * Build the status object.
+ */
+ @NonNull
+ public VerificationStatus build() {
+ return mStatus;
+ }
+ }
+
+ private VerificationStatus(Parcel in) {
+ mIsVerified = in.readBoolean();
+ mAslStatus = in.readInt();
+ mFailuresMessage = in.readString8();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeBoolean(mIsVerified);
+ dest.writeInt(mAslStatus);
+ dest.writeString8(mFailuresMessage);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<VerificationStatus> CREATOR = new Creator<>() {
+ @Override
+ public VerificationStatus createFromParcel(@NonNull Parcel in) {
+ return new VerificationStatus(in);
+ }
+
+ @Override
+ public VerificationStatus[] newArray(int size) {
+ return new VerificationStatus[size];
+ }
+ };
+}
diff --git a/core/java/android/content/pm/verify/pkg/VerifierService.java b/core/java/android/content/pm/verify/pkg/VerifierService.java
new file mode 100644
index 0000000..ccf2119
--- /dev/null
+++ b/core/java/android/content/pm/verify/pkg/VerifierService.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify.pkg;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+
+/**
+ * A base service implementation for the verifier agent to implement.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE)
+public abstract class VerifierService extends Service {
+ /**
+ * Called when a package name is available for a pending verification,
+ * giving the verifier opportunity to pre-fetch any relevant information
+ * that may be needed should a verification for the package be required.
+ */
+ public abstract void onPackageNameAvailable(@NonNull String packageName);
+
+ /**
+ * Called when a package recently provided via {@link #onPackageNameAvailable}
+ * is no longer expected to be installed. This is a hint that any pre-fetch or
+ * cache created as a result of the previous call may be be cleared.
+ * <p>This method will never be called after {@link #onVerificationRequired} is called for the
+ * same package. Once a verification is officially requested by
+ * {@link #onVerificationRequired}, it cannot be cancelled.
+ * </p>
+ */
+ public abstract void onVerificationCancelled(@NonNull String packageName);
+
+ /**
+ * Called when an application needs to be verified. Details about the
+ * verification and actions that can be taken on it will be encapsulated in
+ * the provided {@link VerificationSession} parameter.
+ */
+ public abstract void onVerificationRequired(@NonNull VerificationSession session);
+
+ /**
+ * Called when a verification needs to be retried. This can be encountered
+ * when a prior verification was marked incomplete and the user has indicated
+ * that they've resolved the issue, or when a timeout is reached, but the
+ * the system is attempting to retry. Details about the
+ * verification and actions that can be taken on it will be encapsulated in
+ * the provided {@link VerificationSession} parameter.
+ */
+ public abstract void onVerificationRetry(@NonNull VerificationSession session);
+
+ /**
+ * Called in the case that an active verification has failed. Any APIs called
+ * on the {@link VerificationSession} instance associated with this {@code verificationId} will
+ * throw an {@link IllegalStateException}.
+ */
+ public abstract void onVerificationTimeout(int verificationId);
+
+ /**
+ * Called when the verifier service is bound to the system.
+ */
+ public @Nullable IBinder onBind(@Nullable Intent intent) {
+ if (intent == null || !PackageManager.ACTION_VERIFY_PACKAGE.equals(intent.getAction())) {
+ return null;
+ }
+ return new IVerifierService.Stub() {
+ @Override
+ public void onPackageNameAvailable(@NonNull String packageName) {
+ VerifierService.this.onPackageNameAvailable(packageName);
+ }
+
+ @Override
+ public void onVerificationCancelled(@NonNull String packageName) {
+ VerifierService.this.onVerificationCancelled(packageName);
+ }
+
+ @Override
+ public void onVerificationRequired(@NonNull VerificationSession session) {
+ VerifierService.this.onVerificationRequired(session);
+ }
+
+ @Override
+ public void onVerificationRetry(@NonNull VerificationSession session) {
+ VerifierService.this.onVerificationRetry(session);
+ }
+
+ @Override
+ public void onVerificationTimeout(int verificationId) {
+ VerifierService.this.onVerificationTimeout(verificationId);
+ }
+ };
+ }
+}
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index a5f8199..0af2f25 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -66,3 +66,11 @@
# This flag is read at boot time.
is_fixed_read_only: true
}
+
+flag {
+ name: "dimension_frro"
+ is_exported: true
+ namespace: "resource_manager"
+ description: "Feature flag for passing a dimension to create an frro"
+ bug: "369672322"
+}
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index b11961c..e3fdd26 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -139,6 +139,13 @@
public static final int DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS = 8;
/**
+ * Dialog dismissal due to the system being unable to retrieve a WindowManager instance required
+ * to show the dialog.
+ * @hide
+ */
+ public static final int DISMISSED_REASON_ERROR_NO_WM = 9;
+
+ /**
* @hide
*/
@IntDef({DISMISSED_REASON_BIOMETRIC_CONFIRMED,
@@ -148,7 +155,8 @@
DISMISSED_REASON_ERROR,
DISMISSED_REASON_SERVER_REQUESTED,
DISMISSED_REASON_CREDENTIAL_CONFIRMED,
- DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS})
+ DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS,
+ DISMISSED_REASON_ERROR_NO_WM})
@Retention(RetentionPolicy.SOURCE)
public @interface DismissedReason {}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 1b21bdf..9e3a9b3 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -69,6 +69,7 @@
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -80,6 +81,7 @@
import com.android.internal.util.ArrayUtils;
import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -2157,6 +2159,12 @@
private final Set<Set<DeviceCameraInfo>> mConcurrentCameraIdCombinations = new ArraySet<>();
+ // Diagnostic messages for ArrayIndexOutOfBoundsException in extractCameraIdListLocked
+ // b/367649718
+ private static final int DEVICE_STATUS_ARRAY_SIZE = 10;
+ private final ArrayDeque<String> mDeviceStatusHistory =
+ new ArrayDeque<>(DEVICE_STATUS_ARRAY_SIZE);
+
// Registered availability callbacks and their executors
private final ArrayMap<AvailabilityCallback, Executor> mCallbackMap = new ArrayMap<>();
@@ -2274,6 +2282,10 @@
}
try {
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "connectCameraServiceLocked(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+
CameraStatus[] cameraStatuses = cameraService.addListener(this);
for (CameraStatus cameraStatus : cameraStatuses) {
DeviceCameraInfo info = new DeviceCameraInfo(cameraStatus.cameraId,
@@ -2296,6 +2308,10 @@
}
}
mCameraService = cameraService;
+
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "connectCameraServiceLocked(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
} catch (ServiceSpecificException e) {
// Unexpected failure
throw new IllegalStateException("Failed to register a camera service listener", e);
@@ -2349,18 +2365,28 @@
}
private String[] extractCameraIdListLocked(int deviceId, int devicePolicy) {
- List<String> cameraIds = new ArrayList<>();
- for (int i = 0; i < mDeviceStatus.size(); i++) {
- int status = mDeviceStatus.valueAt(i);
- DeviceCameraInfo info = mDeviceStatus.keyAt(i);
- if (status == ICameraServiceListener.STATUS_NOT_PRESENT
- || status == ICameraServiceListener.STATUS_ENUMERATING
- || shouldHideCamera(deviceId, devicePolicy, info)) {
- continue;
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "extractCameraIdListLocked(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+ try {
+ List<String> cameraIds = new ArrayList<>();
+ for (int i = 0; i < mDeviceStatus.size(); i++) {
+ int status = mDeviceStatus.valueAt(i);
+ DeviceCameraInfo info = mDeviceStatus.keyAt(i);
+ if (status == ICameraServiceListener.STATUS_NOT_PRESENT
+ || status == ICameraServiceListener.STATUS_ENUMERATING
+ || shouldHideCamera(deviceId, devicePolicy, info)) {
+ continue;
+ }
+ cameraIds.add(info.mCameraId);
}
- cameraIds.add(info.mCameraId);
+ return cameraIds.toArray(new String[0]);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ String message = e.getMessage();
+ String messageWithHistory = message + ": {"
+ + String.join(" -> ", mDeviceStatusHistory) + "}";
+ throw new ArrayIndexOutOfBoundsException(messageWithHistory);
}
- return cameraIds.toArray(new String[0]);
}
private Set<Set<String>> extractConcurrentCameraIdListLocked(int deviceId,
@@ -2488,6 +2514,10 @@
synchronized (mLock) {
connectCameraServiceLocked();
try {
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "getCameraIdListNoLazy(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+
// The purpose of the addListener, removeListener pair here is to get a fresh
// list of camera ids from cameraserver. We do this since for in test processes,
// changes can happen w.r.t non-changeable permissions (eg: SYSTEM_CAMERA
@@ -2521,6 +2551,9 @@
onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, info);
mTorchStatus.remove(info);
}
+ addDeviceStatusHistoryLocked(TextUtils.formatSimple(
+ "getCameraIdListNoLazy(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
} catch (ServiceSpecificException e) {
// Unexpected failure
throw new IllegalStateException("Failed to register a camera service listener",
@@ -3209,7 +3242,13 @@
public void onStatusChanged(int status, String cameraId, int deviceId)
throws RemoteException {
synchronized(mLock) {
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("onStatusChanged(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
onStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId));
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("onStatusChanged(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
}
}
@@ -3352,6 +3391,10 @@
*/
public void binderDied() {
synchronized(mLock) {
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("binderDied(E): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
+
// Only do this once per service death
if (mCameraService == null) return;
@@ -3380,6 +3423,10 @@
mConcurrentCameraIdCombinations.clear();
scheduleCameraServiceReconnectionLocked();
+
+ addDeviceStatusHistoryLocked(
+ TextUtils.formatSimple("binderDied(X): tid(%d): mDeviceStatus size %d",
+ Thread.currentThread().getId(), mDeviceStatus.size()));
}
}
@@ -3409,5 +3456,13 @@
return Objects.hash(mCameraId, mDeviceId);
}
}
+
+ private void addDeviceStatusHistoryLocked(String log) {
+ if (mDeviceStatusHistory.size() == DEVICE_STATUS_ARRAY_SIZE) {
+ mDeviceStatusHistory.removeFirst();
+ }
+ mDeviceStatusHistory.addLast(log);
+ }
+
} // CameraManagerGlobal
} // CameraManager
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 7185719..6affd12 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -792,7 +792,6 @@
public void setVirtualDisplaySurface(IVirtualDisplayCallback token, Surface surface) {
try {
mDm.setVirtualDisplaySurface(token, surface);
- setVirtualDisplayState(token, surface != null);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -815,14 +814,6 @@
}
}
- void setVirtualDisplayState(IVirtualDisplayCallback token, boolean isOn) {
- try {
- mDm.setVirtualDisplayState(token, isOn);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- }
-
void setVirtualDisplayRotation(IVirtualDisplayCallback token, @Surface.Rotation int rotation) {
try {
mDm.setVirtualDisplayRotation(token, rotation);
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 73b5d94..e598097 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -431,6 +431,17 @@
*/
public abstract IntArray getDisplayGroupIds();
+
+ /**
+ * Get all display ids belonging to the display group with given id.
+ */
+ public abstract int[] getDisplayIdsForGroup(int groupId);
+
+ /**
+ * Get the mapping of display group ids to the display ids that belong to them.
+ */
+ public abstract SparseArray<int[]> getDisplayIdsByGroupsIds();
+
/**
* Get all available display ids.
*/
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index aa1539f6..b612bca 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -115,9 +115,6 @@
void releaseVirtualDisplay(in IVirtualDisplayCallback token);
// No permissions required but must be same Uid as the creator.
- void setVirtualDisplayState(in IVirtualDisplayCallback token, boolean isOn);
-
- // No permissions required but must be same Uid as the creator.
void setVirtualDisplayRotation(in IVirtualDisplayCallback token, int rotation);
// Get a stable metric for the device's display size. No permissions required.
diff --git a/core/java/android/hardware/display/VirtualDisplay.java b/core/java/android/hardware/display/VirtualDisplay.java
index 6cc938f..32b6405 100644
--- a/core/java/android/hardware/display/VirtualDisplay.java
+++ b/core/java/android/hardware/display/VirtualDisplay.java
@@ -112,18 +112,6 @@
}
/**
- * Sets the on/off state for a virtual display.
- *
- * @param isOn Whether the display should be on or off.
- * @hide
- */
- public void setDisplayState(boolean isOn) {
- if (mToken != null) {
- mGlobal.setVirtualDisplayState(mToken, isOn);
- }
- }
-
- /**
* Sets the rotation of the virtual display.
*
* @param rotation the new rotation of the display. May be one of {@link Surface#ROTATION_0},
diff --git a/core/java/android/net/vcn/VcnTransportInfo.java b/core/java/android/net/vcn/VcnTransportInfo.java
index f546910..1fc91ee 100644
--- a/core/java/android/net/vcn/VcnTransportInfo.java
+++ b/core/java/android/net/vcn/VcnTransportInfo.java
@@ -17,9 +17,11 @@
package android.net.vcn;
import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS;
+import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS;
import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkCapabilities;
@@ -29,6 +31,8 @@
import android.os.Parcelable;
import android.telephony.SubscriptionManager;
+import com.android.internal.util.Preconditions;
+
import java.util.Objects;
/**
@@ -47,6 +51,7 @@
*
* @hide
*/
+// TODO: Do not store WifiInfo and subscription ID in VcnTransportInfo anymore
public class VcnTransportInfo implements TransportInfo, Parcelable {
@Nullable private final WifiInfo mWifiInfo;
private final int mSubId;
@@ -195,4 +200,42 @@
return new VcnTransportInfo[size];
}
};
+
+ /** This class can be used to construct a {@link VcnTransportInfo}. */
+ public static final class Builder {
+ private int mMinUdpPort4500NatTimeoutSeconds = MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET;
+
+ /** Construct Builder */
+ public Builder() {}
+
+ /**
+ * Sets the maximum supported IKEv2/IPsec NATT keepalive timeout.
+ *
+ * <p>This is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs,
+ * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data.
+ *
+ * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN
+ * Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN
+ * Gateway.
+ * @return this {@link Builder} instance, for chaining
+ */
+ @NonNull
+ public Builder setMinUdpPort4500NatTimeoutSeconds(
+ @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS)
+ int minUdpPort4500NatTimeoutSeconds) {
+ Preconditions.checkArgument(
+ minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS,
+ "Timeout must be at least 120s");
+
+ mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds;
+ return Builder.this;
+ }
+
+ /** Build a VcnTransportInfo instance */
+ @NonNull
+ public VcnTransportInfo build() {
+ return new VcnTransportInfo(
+ null /* wifiInfo */, INVALID_SUBSCRIPTION_ID, mMinUdpPort4500NatTimeoutSeconds);
+ }
+ }
}
diff --git a/core/java/android/net/vcn/VcnUtils.java b/core/java/android/net/vcn/VcnUtils.java
new file mode 100644
index 0000000..6dc5180
--- /dev/null
+++ b/core/java/android/net/vcn/VcnUtils.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.vcn;
+
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkSpecifier;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TransportInfo;
+import android.net.wifi.WifiInfo;
+
+import java.util.List;
+
+/**
+ * Utility class for VCN callers get information from VCN network
+ *
+ * @hide
+ */
+public class VcnUtils {
+ /** Get the WifiInfo of the VCN's underlying WiFi network */
+ @Nullable
+ public static WifiInfo getWifiInfoFromVcnCaps(
+ @NonNull ConnectivityManager connectivityMgr,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ final NetworkCapabilities underlyingCaps =
+ getVcnUnderlyingCaps(connectivityMgr, networkCapabilities);
+
+ if (underlyingCaps == null) {
+ return null;
+ }
+
+ final TransportInfo underlyingTransportInfo = underlyingCaps.getTransportInfo();
+ if (!(underlyingTransportInfo instanceof WifiInfo)) {
+ return null;
+ }
+
+ return (WifiInfo) underlyingTransportInfo;
+ }
+
+ /** Get the subscription ID of the VCN's underlying Cell network */
+ public static int getSubIdFromVcnCaps(
+ @NonNull ConnectivityManager connectivityMgr,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ final NetworkCapabilities underlyingCaps =
+ getVcnUnderlyingCaps(connectivityMgr, networkCapabilities);
+
+ if (underlyingCaps == null) {
+ return INVALID_SUBSCRIPTION_ID;
+ }
+
+ final NetworkSpecifier underlyingNetworkSpecifier = underlyingCaps.getNetworkSpecifier();
+ if (!(underlyingNetworkSpecifier instanceof TelephonyNetworkSpecifier)) {
+ return INVALID_SUBSCRIPTION_ID;
+ }
+
+ return ((TelephonyNetworkSpecifier) underlyingNetworkSpecifier).getSubscriptionId();
+ }
+
+ @Nullable
+ private static NetworkCapabilities getVcnUnderlyingCaps(
+ @NonNull ConnectivityManager connectivityMgr,
+ @NonNull NetworkCapabilities networkCapabilities) {
+ // Return null if it is not a VCN network
+ if (networkCapabilities.getTransportInfo() == null
+ || !(networkCapabilities.getTransportInfo() instanceof VcnTransportInfo)) {
+ return null;
+ }
+
+ // As of Android 16, VCN has one underlying network, and only one. If there are more
+ // than one networks due to future changes in the VCN mainline code, just take the first
+ // network
+ final List<Network> underlyingNws = networkCapabilities.getUnderlyingNetworks();
+ if (underlyingNws == null) {
+ return null;
+ }
+
+ return connectivityMgr.getNetworkCapabilities(underlyingNws.get(0));
+ }
+}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index a8267d1..479aa98 100644
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressAutoDoc;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.ActivityThread;
@@ -401,33 +402,42 @@
* device. This value never changes while a device is booted, but it may
* increase when the hardware manufacturer provides an OTA update.
* <p>
- * Together with {@link SDK_MINOR_INT}, this constant defines the
- * <pre>major.minor</pre> version of Android. <pre>SDK_INT</pre> is
- * increased and <pre>SDK_MINOR_INT</pre> is set to 0 on new Android
- * dessert releases. Between these, Android may also release so called
- * minor releases where <pre>SDK_INT</pre> remains unchanged and
- * <pre>SDK_MINOR_INT</pre> is increased. Minor releases can add new
- * APIs, and have stricter guarantees around backwards compatibility
- * (e.g. no changes gated by <pre>targetSdkVersion</pre>) compared to
- * major releases.
+ * This constant records the major version of Android. Use {@link
+ * SDK_INT_FULL} if you need to consider the minor version of Android
+ * as well.
* <p>
* Possible values are defined in {@link Build.VERSION_CODES}.
+ * @see #SDK_INT_FULL
*/
public static final int SDK_INT = SystemProperties.getInt(
"ro.build.version.sdk", 0);
/**
- * The minor SDK version of the software currently running on this hardware
- * device. This value never changes while a device is booted, but it may
- * increase when the hardware manufacturer provides an OTA update.
+ * The major and minor SDK version of the software currently running on
+ * this hardware device. This value never changes while a device is
+ * booted, but it may increase when the hardware manufacturer provides
+ * an OTA update.
* <p>
- * Together with {@link SDK_INT}, this constant defines the
- * <pre>major.minor</pre> version of Android. See {@link SDK_INT} for
- * more information.
+ * <code>SDK_INT</code> is increased on new Android dessert releases,
+ * also called major releases. Between these, Android may also release
+ * minor releases where <code>SDK_INT</code> remains unchanged. Minor
+ * releases can add new APIs, and have stricter guarantees around
+ * backwards compatibility (e.g. no changes gated by
+ * <code>targetSdkVersion</code>) compared to major releases.
+ * <p>
+ * <code>SDK_INT_FULL</code> is increased on every release.
+ * <p>
+ * Possible values are defined in {@link
+ * android.os.Build.VERSION_CODES_FULL}.
*/
@FlaggedApi(Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME)
- public static final int SDK_MINOR_INT = SystemProperties.getInt(
- "ro.build.version.sdk_minor", 0);
+ public static final int SDK_INT_FULL;
+
+ static {
+ SDK_INT_FULL = VERSION_CODES_FULL.SDK_INT_MULTIPLIER
+ * SystemProperties.getInt("ro.build.version.sdk", 0)
+ + SystemProperties.getInt("ro.build.version.sdk_minor", 0);
+ }
/**
* The SDK version of the software that <em>initially</em> shipped on
@@ -1264,6 +1274,25 @@
}
/**
+ * Enumeration of the currently known SDK major and minor version codes.
+ * The numbers increase for every release, and are guaranteed to be ordered
+ * by the release date of each release. The actual values should be
+ * considered an implementation detail, and the current encoding scheme may
+ * change in the future.
+ *
+ * @see android.os.Build.VERSION#SDK_INT_FULL
+ */
+ @FlaggedApi(Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME)
+ @SuppressLint("AcronymName")
+ public static class VERSION_CODES_FULL {
+ private VERSION_CODES_FULL() {}
+
+ // Use the last 5 digits for the minor version. This allows the
+ // minor version to be set to CUR_DEVELOPMENT.
+ private static final int SDK_INT_MULTIPLIER = 100000;
+ }
+
+ /**
* The vendor API for 2024 Q2
*
* <p>For Android 14-QPR3 and later, the vendor API level is completely decoupled from the SDK
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index beb9a93..2d3dd1b 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -598,6 +598,11 @@
final String abi = chooseAbi(angleInfo);
// Build a path that includes installed native libs and APK
+ // TODO (b/370113081): If the native libraries are not found in this path,
+ // the system libraries will be loaded instead.
+ // This can happen if the ANGLE APK is present,
+ // but accidentally packaged without native libraries.
+ // TBD if this should fail instead of falling back to the system version.
final String paths = angleInfo.nativeLibraryDir
+ File.pathSeparator
+ angleInfo.sourceDir
diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java
index 2cb86f7..769cbdd 100644
--- a/core/java/android/os/RemoteCallbackList.java
+++ b/core/java/android/os/RemoteCallbackList.java
@@ -92,9 +92,9 @@
/**
* Add a new callback to the list. This callback will remain in the list
* until a corresponding call to {@link #unregister} or its hosting process
- * goes away. If the callback was already registered (determined by
+ * goes away. If the callback was already registered (determined by
* checking to see if the {@link IInterface#asBinder callback.asBinder()}
- * object is already in the list), then it will be left as-is.
+ * object is already in the list), then it will be replaced with the new callback.
* Registrations are not counted; a single call to {@link #unregister}
* will remove a callback after any number calls to register it.
*
@@ -106,7 +106,7 @@
*
* @param cookie Optional additional data to be associated with this
* callback.
- *
+ *
* @return Returns true if the callback was successfully added to the list.
* Returns false if it was not added, either because {@link #kill} had
* previously been called or the callback's process has gone away.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d82af55..a2c41c1 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2092,23 +2092,6 @@
public static final String ACTION_ZEN_MODE_SETTINGS = "android.settings.ZEN_MODE_SETTINGS";
/**
- * Activity Action: Show Zen Mode visual effects configuration settings.
- *
- * @hide
- */
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ZEN_MODE_BLOCKED_EFFECTS_SETTINGS =
- "android.settings.ZEN_MODE_BLOCKED_EFFECTS_SETTINGS";
-
- /**
- * Activity Action: Show Zen Mode onboarding activity.
- *
- * @hide
- */
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ZEN_MODE_ONBOARDING = "android.settings.ZEN_MODE_ONBOARDING";
-
- /**
* Activity Action: Show Zen Mode (aka Do Not Disturb) priority configuration settings.
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@@ -8748,35 +8731,6 @@
/** @hide */ public static final int ZEN_DURATION_FOREVER = 0;
/**
- * If nonzero, will show the zen upgrade notification when the user toggles DND on/off.
- * @hide
- */
- @Readable
- public static final String SHOW_ZEN_UPGRADE_NOTIFICATION = "show_zen_upgrade_notification";
-
- /**
- * If nonzero, will show the zen update settings suggestion.
- * @hide
- */
- @Readable
- public static final String SHOW_ZEN_SETTINGS_SUGGESTION = "show_zen_settings_suggestion";
-
- /**
- * If nonzero, zen has not been updated to reflect new changes.
- * @hide
- */
- @Readable
- public static final String ZEN_SETTINGS_UPDATED = "zen_settings_updated";
-
- /**
- * If nonzero, zen setting suggestion has been viewed by user
- * @hide
- */
- @Readable
- public static final String ZEN_SETTINGS_SUGGESTION_VIEWED =
- "zen_settings_suggestion_viewed";
-
- /**
* Whether the in call notification is enabled to play sound during calls. The value is
* boolean (1 or 0).
* @hide
@@ -18072,10 +18026,6 @@
MOVED_TO_SECURE = new HashSet<>(8);
MOVED_TO_SECURE.add(Global.INSTALL_NON_MARKET_APPS);
MOVED_TO_SECURE.add(Global.ZEN_DURATION);
- MOVED_TO_SECURE.add(Global.SHOW_ZEN_UPGRADE_NOTIFICATION);
- MOVED_TO_SECURE.add(Global.SHOW_ZEN_SETTINGS_SUGGESTION);
- MOVED_TO_SECURE.add(Global.ZEN_SETTINGS_UPDATED);
- MOVED_TO_SECURE.add(Global.ZEN_SETTINGS_SUGGESTION_VIEWED);
MOVED_TO_SECURE.add(Global.CHARGING_SOUNDS_ENABLED);
MOVED_TO_SECURE.add(Global.CHARGING_VIBRATION_ENABLED);
MOVED_TO_SECURE.add(Global.NOTIFICATION_BUBBLES);
@@ -18910,40 +18860,6 @@
@Readable
public static final String SHOW_MUTE_IN_CRASH_DIALOG = "show_mute_in_crash_dialog";
-
- /**
- * If nonzero, will show the zen upgrade notification when the user toggles DND on/off.
- * @hide
- * @deprecated - Use {@link android.provider.Settings.Secure#SHOW_ZEN_UPGRADE_NOTIFICATION}
- */
- @Deprecated
- public static final String SHOW_ZEN_UPGRADE_NOTIFICATION = "show_zen_upgrade_notification";
-
- /**
- * If nonzero, will show the zen update settings suggestion.
- * @hide
- * @deprecated - Use {@link android.provider.Settings.Secure#SHOW_ZEN_SETTINGS_SUGGESTION}
- */
- @Deprecated
- public static final String SHOW_ZEN_SETTINGS_SUGGESTION = "show_zen_settings_suggestion";
-
- /**
- * If nonzero, zen has not been updated to reflect new changes.
- * @deprecated - Use {@link android.provider.Settings.Secure#ZEN_SETTINGS_UPDATED}
- * @hide
- */
- @Deprecated
- public static final String ZEN_SETTINGS_UPDATED = "zen_settings_updated";
-
- /**
- * If nonzero, zen setting suggestion has been viewed by user
- * @hide
- * @deprecated - Use {@link android.provider.Settings.Secure#ZEN_SETTINGS_SUGGESTION_VIEWED}
- */
- @Deprecated
- public static final String ZEN_SETTINGS_SUGGESTION_VIEWED =
- "zen_settings_suggestion_viewed";
-
/**
* Backup and restore agent timeout parameters.
* These parameters are represented by a comma-delimited key-value list.
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 48d7cf7..7b7058e 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -393,6 +393,23 @@
}
}
+ /**
+ * Informs the notification manager about what {@link Adjustment Adjustments} are supported by
+ * this NAS.
+ *
+ * For backwards compatibility, we assume all Adjustment types are supported by the NAS.
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public final void setAdjustmentTypeSupportedState(@NonNull @Adjustment.Keys String key,
+ boolean supported) {
+ if (!isBound()) return;
+ try {
+ getNotificationInterface().setAdjustmentTypeSupportedState(mWrapper, key, supported);
+ } catch (android.os.RemoteException ex) {
+ Log.v(TAG, "Unable to contact notification manager", ex);
+ }
+ }
+
private class NotificationAssistantServiceWrapper extends NotificationListenerWrapper {
@Override
public void onNotificationEnqueuedWithChannel(IStatusBarNotificationHolder sbnHolder,
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 60a7d6b..c9f4647 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -16,6 +16,7 @@
package android.service.notification;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.Flags;
@@ -24,6 +25,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Set;
@@ -62,6 +64,7 @@
public static class FieldDiff<T> {
private final T mFrom;
private final T mTo;
+ private final BaseDiff mDetailedDiff;
/**
* Constructor to create a FieldDiff object with the given values.
@@ -71,6 +74,19 @@
public FieldDiff(@Nullable T from, @Nullable T to) {
mFrom = from;
mTo = to;
+ mDetailedDiff = null;
+ }
+
+ /**
+ * Constructor to create a FieldDiff object with the given values, and that has a
+ * detailed BaseDiff.
+ * @param from from (old) value
+ * @param to to (new) value
+ */
+ public FieldDiff(@Nullable T from, @Nullable T to, @Nullable BaseDiff detailedDiff) {
+ mFrom = from;
+ mTo = to;
+ mDetailedDiff = detailedDiff;
}
/**
@@ -92,6 +108,9 @@
*/
@Override
public String toString() {
+ if (mDetailedDiff != null) {
+ return mDetailedDiff.toString();
+ }
return mFrom + "->" + mTo;
}
@@ -99,6 +118,9 @@
* Returns whether this represents an actual diff.
*/
public boolean hasDiff() {
+ if (mDetailedDiff != null) {
+ return mDetailedDiff.hasDiff();
+ }
// note that Objects.equals handles null values gracefully.
return !Objects.equals(mFrom, mTo);
}
@@ -114,7 +136,8 @@
@ExistenceChange private int mExists = NONE;
// Map from field name to diffs for any standalone fields in the object.
- private ArrayMap<String, FieldDiff> mFields = new ArrayMap<>();
+ // LinkedHashMap is specifically chosen here to show insertion order when keys are fetched.
+ private LinkedHashMap<String, FieldDiff> mFields = new LinkedHashMap<>();
// Functions for actually diffing objects and string representations have to be implemented
// by subclasses.
@@ -549,8 +572,16 @@
if (!Objects.equals(from.enabler, to.enabler)) {
addField(FIELD_ENABLER, new FieldDiff<>(from.enabler, to.enabler));
}
- if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
- addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
+ if (android.app.Flags.modesApi()) {
+ PolicyDiff policyDiff = new PolicyDiff(from.zenPolicy, to.zenPolicy);
+ if (policyDiff.hasDiff()) {
+ addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy,
+ policyDiff));
+ }
+ } else {
+ if (!Objects.equals(from.zenPolicy, to.zenPolicy)) {
+ addField(FIELD_ZEN_POLICY, new FieldDiff<>(from.zenPolicy, to.zenPolicy));
+ }
}
if (from.modified != to.modified) {
addField(FIELD_MODIFIED, new FieldDiff<>(from.modified, to.modified));
@@ -559,9 +590,12 @@
addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
}
if (android.app.Flags.modesApi()) {
- if (!Objects.equals(from.zenDeviceEffects, to.zenDeviceEffects)) {
+ DeviceEffectsDiff deviceEffectsDiff = new DeviceEffectsDiff(from.zenDeviceEffects,
+ to.zenDeviceEffects);
+ if (deviceEffectsDiff.hasDiff()) {
addField(FIELD_ZEN_DEVICE_EFFECTS,
- new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects));
+ new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects,
+ deviceEffectsDiff));
}
if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
addField(FIELD_TRIGGER_DESCRIPTION,
@@ -629,7 +663,7 @@
sb.append(key);
sb.append(":");
- sb.append(diff);
+ sb.append(diff.toString());
}
if (becameActive()) {
@@ -663,4 +697,338 @@
return mActiveDiff != null && !mActiveDiff.to();
}
}
+
+ /**
+ * Diff class representing a change between two
+ * {@link android.service.notification.ZenDeviceEffects}.
+ */
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ public static class DeviceEffectsDiff extends BaseDiff {
+ public static final String FIELD_GRAYSCALE = "mGrayscale";
+ public static final String FIELD_SUPPRESS_AMBIENT_DISPLAY = "mSuppressAmbientDisplay";
+ public static final String FIELD_DIM_WALLPAPER = "mDimWallpaper";
+ public static final String FIELD_NIGHT_MODE = "mNightMode";
+ public static final String FIELD_DISABLE_AUTO_BRIGHTNESS = "mDisableAutoBrightness";
+ public static final String FIELD_DISABLE_TAP_TO_WAKE = "mDisableTapToWake";
+ public static final String FIELD_DISABLE_TILT_TO_WAKE = "mDisableTiltToWake";
+ public static final String FIELD_DISABLE_TOUCH = "mDisableTouch";
+ public static final String FIELD_MINIMIZE_RADIO_USAGE = "mMinimizeRadioUsage";
+ public static final String FIELD_MAXIMIZE_DOZE = "mMaximizeDoze";
+ public static final String FIELD_EXTRA_EFFECTS = "mExtraEffects";
+ // NOTE: new field strings must match the variable names in ZenDeviceEffects
+
+ /**
+ * Create a DeviceEffectsDiff representing the difference between two ZenDeviceEffects
+ * objects.
+ * @param from previous ZenDeviceEffects
+ * @param to new ZenDeviceEffects
+ * @return The diff between the two given ZenDeviceEffects
+ */
+ public DeviceEffectsDiff(ZenDeviceEffects from, ZenDeviceEffects to) {
+ super(from, to);
+ // Short-circuit the both-null case
+ if (from == null && to == null) {
+ return;
+ }
+ if (hasExistenceChange()) {
+ // either added or removed; return here. otherwise (they're not both null) there's
+ // field diffs.
+ return;
+ }
+
+ // Compare all fields, knowing there's some diff and that neither is null.
+ if (from.shouldDisplayGrayscale() != to.shouldDisplayGrayscale()) {
+ addField(FIELD_GRAYSCALE, new FieldDiff<>(from.shouldDisplayGrayscale(),
+ to.shouldDisplayGrayscale()));
+ }
+ if (from.shouldSuppressAmbientDisplay() != to.shouldSuppressAmbientDisplay()) {
+ addField(FIELD_SUPPRESS_AMBIENT_DISPLAY,
+ new FieldDiff<>(from.shouldSuppressAmbientDisplay(),
+ to.shouldSuppressAmbientDisplay()));
+ }
+ if (from.shouldDimWallpaper() != to.shouldDimWallpaper()) {
+ addField(FIELD_DIM_WALLPAPER, new FieldDiff<>(from.shouldDimWallpaper(),
+ to.shouldDimWallpaper()));
+ }
+ if (from.shouldUseNightMode() != to.shouldUseNightMode()) {
+ addField(FIELD_NIGHT_MODE, new FieldDiff<>(from.shouldUseNightMode(),
+ to.shouldUseNightMode()));
+ }
+ if (from.shouldDisableAutoBrightness() != to.shouldDisableAutoBrightness()) {
+ addField(FIELD_DISABLE_AUTO_BRIGHTNESS,
+ new FieldDiff<>(from.shouldDisableAutoBrightness(),
+ to.shouldDisableAutoBrightness()));
+ }
+ if (from.shouldDisableTapToWake() != to.shouldDisableTapToWake()) {
+ addField(FIELD_DISABLE_TAP_TO_WAKE, new FieldDiff<>(from.shouldDisableTapToWake(),
+ to.shouldDisableTapToWake()));
+ }
+ if (from.shouldDisableTiltToWake() != to.shouldDisableTiltToWake()) {
+ addField(FIELD_DISABLE_TILT_TO_WAKE,
+ new FieldDiff<>(from.shouldDisableTiltToWake(),
+ to.shouldDisableTiltToWake()));
+ }
+ if (from.shouldDisableTouch() != to.shouldDisableTouch()) {
+ addField(FIELD_DISABLE_TOUCH, new FieldDiff<>(from.shouldDisableTouch(),
+ to.shouldDisableTouch()));
+ }
+ if (from.shouldMinimizeRadioUsage() != to.shouldMinimizeRadioUsage()) {
+ addField(FIELD_MINIMIZE_RADIO_USAGE,
+ new FieldDiff<>(from.shouldMinimizeRadioUsage(),
+ to.shouldMinimizeRadioUsage()));
+ }
+ if (from.shouldMaximizeDoze() != to.shouldMaximizeDoze()) {
+ addField(FIELD_MAXIMIZE_DOZE, new FieldDiff<>(from.shouldMaximizeDoze(),
+ to.shouldMaximizeDoze()));
+ }
+ if (!Objects.equals(from.getExtraEffects(), to.getExtraEffects())) {
+ addField(FIELD_EXTRA_EFFECTS, new FieldDiff<>(from.getExtraEffects(),
+ to.getExtraEffects()));
+ }
+ }
+
+ /**
+ * Returns whether this object represents an actual diff.
+ */
+ @Override
+ public boolean hasDiff() {
+ return hasExistenceChange() || hasFieldDiffs();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ZenDeviceEffectsDiff{");
+ if (!hasDiff()) {
+ sb.append("no changes");
+ }
+
+ // If added or deleted, we just append that.
+ if (hasExistenceChange()) {
+ if (wasAdded()) {
+ sb.append("added");
+ } else if (wasRemoved()) {
+ sb.append("removed");
+ }
+ }
+
+ // Append all of the individual field diffs
+ boolean first = true;
+ for (String key : fieldNamesWithDiff()) {
+ FieldDiff diff = getDiffForField(key);
+ if (diff == null) {
+ // The diff should not have null diffs added, but we add this to be defensive.
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+
+ sb.append(key);
+ sb.append(":");
+ sb.append(diff);
+ }
+
+ return sb.append("}").toString();
+ }
+ }
+
+ /**
+ * Diff class representing a change between two {@link android.service.notification.ZenPolicy}.
+ */
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ public static class PolicyDiff extends BaseDiff {
+ public static final String FIELD_PRIORITY_CATEGORY_REMINDERS =
+ "mPriorityCategories_Reminders";
+ public static final String FIELD_PRIORITY_CATEGORY_EVENTS = "mPriorityCategories_Events";
+ public static final String FIELD_PRIORITY_CATEGORY_MESSAGES =
+ "mPriorityCategories_Messages";
+ public static final String FIELD_PRIORITY_CATEGORY_CALLS = "mPriorityCategories_Calls";
+ public static final String FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS =
+ "mPriorityCategories_RepeatCallers";
+ public static final String FIELD_PRIORITY_CATEGORY_ALARMS = "mPriorityCategories_Alarms";
+ public static final String FIELD_PRIORITY_CATEGORY_MEDIA = "mPriorityCategories_Media";
+ public static final String FIELD_PRIORITY_CATEGORY_SYSTEM = "mPriorityCategories_System";
+ public static final String FIELD_PRIORITY_CATEGORY_CONVERSATIONS =
+ "mPriorityCategories_Conversations";
+
+ public static final String FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT =
+ "mVisualEffects_FullScreenIntent";
+ public static final String FIELD_VISUAL_EFFECT_LIGHTS = "mVisualEffects_Lights";
+ public static final String FIELD_VISUAL_EFFECT_PEEK = "mVisualEffects_Peek";
+ public static final String FIELD_VISUAL_EFFECT_STATUS_BAR = "mVisualEffects_StatusBar";
+ public static final String FIELD_VISUAL_EFFECT_BADGE = "mVisualEffects_Badge";
+ public static final String FIELD_VISUAL_EFFECT_AMBIENT = "mVisualEffects_Ambient";
+ public static final String FIELD_VISUAL_EFFECT_NOTIFICATION_LIST =
+ "mVisualEffects_NotificationList";
+
+ public static final String FIELD_PRIORITY_MESSAGES = "mPriorityMessages";
+ public static final String FIELD_PRIORITY_CALLS = "mPriorityCalls";
+ public static final String FIELD_CONVERSATION_SENDERS = "mConversationSenders";
+ public static final String FIELD_ALLOW_CHANNELS = "mAllowChannels";
+
+ /**
+ * Create a PolicyDiff representing the difference between two ZenPolicy objects.
+ *
+ * @param from previous ZenPolicy
+ * @param to new ZenPolicy
+ * @return The diff between the two given ZenPolicy
+ */
+ public PolicyDiff(ZenPolicy from, ZenPolicy to) {
+ super(from, to);
+ // Short-circuit the both-null case
+ if (from == null && to == null) {
+ return;
+ }
+ if (hasExistenceChange()) {
+ // either added or removed; return here. otherwise (they're not both null) there's
+ // field diffs.
+ return;
+ }
+
+ // Compare all fields, knowing there's some diff and that neither is null.
+ if (from.getPriorityCategoryReminders() != to.getPriorityCategoryReminders()) {
+ addField(FIELD_PRIORITY_CATEGORY_REMINDERS,
+ new FieldDiff<>(from.getPriorityCategoryReminders(),
+ to.getPriorityCategoryReminders()));
+ }
+ if (from.getPriorityCategoryEvents() != to.getPriorityCategoryEvents()) {
+ addField(FIELD_PRIORITY_CATEGORY_EVENTS,
+ new FieldDiff<>(from.getPriorityCategoryEvents(),
+ to.getPriorityCategoryEvents()));
+ }
+ if (from.getPriorityCategoryMessages() != to.getPriorityCategoryMessages()) {
+ addField(FIELD_PRIORITY_CATEGORY_MESSAGES,
+ new FieldDiff<>(from.getPriorityCategoryMessages(),
+ to.getPriorityCategoryMessages()));
+ }
+ if (from.getPriorityCategoryCalls() != to.getPriorityCategoryCalls()) {
+ addField(FIELD_PRIORITY_CATEGORY_CALLS,
+ new FieldDiff<>(from.getPriorityCategoryCalls(),
+ to.getPriorityCategoryCalls()));
+ }
+ if (from.getPriorityCategoryRepeatCallers() != to.getPriorityCategoryRepeatCallers()) {
+ addField(FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS,
+ new FieldDiff<>(from.getPriorityCategoryRepeatCallers(),
+ to.getPriorityCategoryRepeatCallers()));
+ }
+ if (from.getPriorityCategoryAlarms() != to.getPriorityCategoryAlarms()) {
+ addField(FIELD_PRIORITY_CATEGORY_ALARMS,
+ new FieldDiff<>(from.getPriorityCategoryAlarms(),
+ to.getPriorityCategoryAlarms()));
+ }
+ if (from.getPriorityCategoryMedia() != to.getPriorityCategoryMedia()) {
+ addField(FIELD_PRIORITY_CATEGORY_MEDIA,
+ new FieldDiff<>(from.getPriorityCategoryMedia(),
+ to.getPriorityCategoryMedia()));
+ }
+ if (from.getPriorityCategorySystem() != to.getPriorityCategorySystem()) {
+ addField(FIELD_PRIORITY_CATEGORY_SYSTEM,
+ new FieldDiff<>(from.getPriorityCategorySystem(),
+ to.getPriorityCategorySystem()));
+ }
+ if (from.getPriorityCategoryConversations() != to.getPriorityCategoryConversations()) {
+ addField(FIELD_PRIORITY_CATEGORY_CONVERSATIONS,
+ new FieldDiff<>(from.getPriorityCategoryConversations(),
+ to.getPriorityCategoryConversations()));
+ }
+ if (from.getVisualEffectFullScreenIntent() != to.getVisualEffectFullScreenIntent()) {
+ addField(FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT,
+ new FieldDiff<>(from.getVisualEffectFullScreenIntent(),
+ to.getVisualEffectFullScreenIntent()));
+ }
+ if (from.getVisualEffectLights() != to.getVisualEffectLights()) {
+ addField(FIELD_VISUAL_EFFECT_LIGHTS,
+ new FieldDiff<>(from.getVisualEffectLights(), to.getVisualEffectLights()));
+ }
+ if (from.getVisualEffectPeek() != to.getVisualEffectPeek()) {
+ addField(FIELD_VISUAL_EFFECT_PEEK, new FieldDiff<>(from.getVisualEffectPeek(),
+ to.getVisualEffectPeek()));
+ }
+ if (from.getVisualEffectStatusBar() != to.getVisualEffectStatusBar()) {
+ addField(FIELD_VISUAL_EFFECT_STATUS_BAR,
+ new FieldDiff<>(from.getVisualEffectStatusBar(),
+ to.getVisualEffectStatusBar()));
+ }
+ if (from.getVisualEffectBadge() != to.getVisualEffectBadge()) {
+ addField(FIELD_VISUAL_EFFECT_BADGE, new FieldDiff<>(from.getVisualEffectBadge(),
+ to.getVisualEffectBadge()));
+ }
+ if (from.getVisualEffectAmbient() != to.getVisualEffectAmbient()) {
+ addField(FIELD_VISUAL_EFFECT_AMBIENT, new FieldDiff<>(from.getVisualEffectAmbient(),
+ to.getVisualEffectAmbient()));
+ }
+ if (from.getVisualEffectNotificationList() != to.getVisualEffectNotificationList()) {
+ addField(FIELD_VISUAL_EFFECT_NOTIFICATION_LIST,
+ new FieldDiff<>(from.getVisualEffectNotificationList(),
+ to.getVisualEffectNotificationList()));
+ }
+ if (from.getPriorityMessageSenders() != to.getPriorityMessageSenders()) {
+ addField(FIELD_PRIORITY_MESSAGES, new FieldDiff<>(from.getPriorityMessageSenders(),
+ to.getPriorityMessageSenders()));
+ }
+ if (from.getPriorityCallSenders() != to.getPriorityCallSenders()) {
+ addField(FIELD_PRIORITY_CALLS, new FieldDiff<>(from.getPriorityCallSenders(),
+ to.getPriorityCallSenders()));
+ }
+ if (from.getPriorityConversationSenders() != to.getPriorityConversationSenders()) {
+ addField(FIELD_CONVERSATION_SENDERS,
+ new FieldDiff<>(from.getPriorityConversationSenders(),
+ to.getPriorityConversationSenders()));
+ }
+ if (from.getPriorityChannelsAllowed() != to.getPriorityChannelsAllowed()) {
+ addField(FIELD_ALLOW_CHANNELS, new FieldDiff<>(from.getPriorityChannelsAllowed(),
+ to.getPriorityChannelsAllowed()));
+ }
+ }
+
+ /**
+ * Returns whether this object represents an actual diff.
+ */
+ @Override
+ public boolean hasDiff() {
+ return hasExistenceChange() || hasFieldDiffs();
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ZenPolicyDiff{");
+ // The diff should not have null diffs added, but we add this to be defensive.
+ if (!hasDiff()) {
+ sb.append("no changes");
+ }
+
+ // If added or deleted, we just append that.
+ if (hasExistenceChange()) {
+ if (wasAdded()) {
+ sb.append("added");
+ } else if (wasRemoved()) {
+ sb.append("removed");
+ }
+ }
+
+ // Go through all of the individual fields
+ boolean first = true;
+ for (String key : fieldNamesWithDiff()) {
+ FieldDiff diff = getDiffForField(key);
+ if (diff == null) {
+ // this shouldn't happen...
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ sb.append(", ");
+ }
+
+ sb.append(key);
+ sb.append(":");
+ sb.append(diff);
+ }
+
+ return sb.append("}").toString();
+ }
+ }
+
}
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index be0d7b3..4cff67e 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -29,6 +29,8 @@
import android.os.Parcelable;
import android.util.proto.ProtoOutputStream;
+import androidx.annotation.VisibleForTesting;
+
import java.io.ByteArrayOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -207,8 +209,10 @@
/**
* Total number of priority categories. Keep updated with any updates to PriorityCategory enum.
+ * If this changes, you must update {@link ZenModeDiff.PolicyDiff} to include new categories.
* @hide
*/
+ @VisibleForTesting
public static final int NUM_PRIORITY_CATEGORIES = 9;
/** @hide */
@@ -241,8 +245,10 @@
/**
* Total number of visual effects. Keep updated with any updates to VisualEffect enum.
+ * If this changes, you must update {@link ZenModeDiff.PolicyDiff} to include new categories.
* @hide
*/
+ @VisibleForTesting
public static final int NUM_VISUAL_EFFECTS = 7;
/** @hide */
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 3599332..ec5b488 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -178,3 +178,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "handwriting_unsupported_show_soft_input_fix"
+ namespace: "text"
+ description: "Don't show soft keyboard on stylus input if text field doesn't support handwriting and getShowSoftInputOnFocus() returns false."
+ bug: "363180475"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/tracing/TEST_MAPPING b/core/java/android/tracing/TEST_MAPPING
new file mode 100644
index 0000000..b51d19d
--- /dev/null
+++ b/core/java/android/tracing/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+ "postsubmit": [
+ {
+ "name": "TracingTests"
+ },
+ {
+ "name": "ProtologPerfTests"
+ }
+ ]
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index f132963..c217999 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -19,6 +19,7 @@
import static com.android.text.flags.Flags.handwritingCursorPosition;
import static com.android.text.flags.Flags.handwritingTrackDisabled;
import static com.android.text.flags.Flags.handwritingUnsupportedMessage;
+import static com.android.text.flags.Flags.handwritingUnsupportedShowSoftInputFix;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
@@ -241,7 +242,11 @@
if (!candidateView.hasFocus()) {
requestFocusWithoutReveal(candidateView);
}
- mImm.showSoftInput(candidateView, 0);
+ if (!handwritingUnsupportedShowSoftInputFix()
+ || (candidateView instanceof TextView tv
+ && tv.getShowSoftInputOnFocus())) {
+ mImm.showSoftInput(candidateView, 0);
+ }
mState.mHandled = true;
mState.mShouldInitHandwriting = false;
motionEvent.setAction((motionEvent.getAction()
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 40a75fd..8fb17c7 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -238,7 +238,7 @@
import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
-import android.view.accessibility.AccessibilityManager.HighTextContrastChangeListener;
+import android.view.accessibility.AccessibilityManager.HighContrastTextStateChangeListener;
import android.view.accessibility.AccessibilityNodeIdManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
@@ -1800,8 +1800,8 @@
}
mAccessibilityManager.addAccessibilityStateChangeListener(
mAccessibilityInteractionConnectionManager, mHandler);
- mAccessibilityManager.addHighTextContrastStateChangeListener(
- mHighContrastTextManager, mHandler);
+ mAccessibilityManager.addHighContrastTextStateChangeListener(
+ mExecutor, mHighContrastTextManager);
DisplayManagerGlobal
.getInstance()
.registerDisplayListener(
@@ -1838,7 +1838,7 @@
private void unregisterListeners() {
mAccessibilityManager.removeAccessibilityStateChangeListener(
mAccessibilityInteractionConnectionManager);
- mAccessibilityManager.removeHighTextContrastStateChangeListener(
+ mAccessibilityManager.removeHighContrastTextStateChangeListener(
mHighContrastTextManager);
DisplayManagerGlobal
.getInstance()
@@ -11907,12 +11907,12 @@
}
}
- final class HighContrastTextManager implements HighTextContrastChangeListener {
+ final class HighContrastTextManager implements HighContrastTextStateChangeListener {
HighContrastTextManager() {
- ThreadedRenderer.setHighContrastText(mAccessibilityManager.isHighTextContrastEnabled());
+ ThreadedRenderer.setHighContrastText(mAccessibilityManager.isHighContrastTextEnabled());
}
@Override
- public void onHighTextContrastStateChanged(boolean enabled) {
+ public void onHighContrastTextStateChanged(boolean enabled) {
ThreadedRenderer.setHighContrastText(enabled);
destroyAndInvalidate();
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 2b7cf42..fd57aec 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -253,7 +253,7 @@
boolean mIsTouchExplorationEnabled;
@UnsupportedAppUsage(trackingBug = 123768939L)
- boolean mIsHighTextContrastEnabled;
+ boolean mIsHighContrastTextEnabled;
boolean mIsAudioDescriptionByDefaultRequested;
@@ -276,8 +276,8 @@
private final ArrayMap<TouchExplorationStateChangeListener, Handler>
mTouchExplorationStateChangeListeners = new ArrayMap<>();
- private final ArrayMap<HighTextContrastChangeListener, Handler>
- mHighTextContrastStateChangeListeners = new ArrayMap<>();
+ private final ArrayMap<HighContrastTextStateChangeListener, Executor>
+ mHighContrastTextStateChangeListeners = new ArrayMap<>();
private final ArrayMap<AccessibilityServicesStateChangeListener, Executor>
mServicesStateChangeListeners = new ArrayMap<>();
@@ -356,21 +356,20 @@
}
/**
- * Listener for the system high text contrast state. To listen for changes to
- * the high text contrast state on the device, implement this interface and
+ * Listener for the system high contrast text state. To listen for changes to
+ * the high contrast text state on the device, implement this interface and
* register it with the system by calling
- * {@link #addHighTextContrastStateChangeListener}.
- *
- * @hide
+ * {@link #addHighContrastTextStateChangeListener}.
*/
- public interface HighTextContrastChangeListener {
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public interface HighContrastTextStateChangeListener {
/**
- * Called when the high text contrast enabled state changes.
+ * Called when the high contrast text enabled state changes.
*
- * @param enabled Whether high text contrast is enabled.
+ * @param enabled Whether high contrast text is enabled.
*/
- void onHighTextContrastStateChanged(boolean enabled);
+ void onHighContrastTextStateChanged(boolean enabled);
}
/**
@@ -655,24 +654,23 @@
}
/**
- * Returns if the high text contrast in the system is enabled.
+ * Returns if high contrast text in the system is enabled.
* <p>
* <strong>Note:</strong> You need to query this only if you application is
* doing its own rendering and does not rely on the platform rendering pipeline.
* </p>
*
- * @return True if high text contrast is enabled, false otherwise.
+ * @return True if high contrast text is enabled, false otherwise.
*
- * @hide
*/
- @UnsupportedAppUsage
- public boolean isHighTextContrastEnabled() {
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public boolean isHighContrastTextEnabled() {
synchronized (mLock) {
IAccessibilityManager service = getServiceLocked();
if (service == null) {
return false;
}
- return mIsHighTextContrastEnabled;
+ return mIsHighContrastTextEnabled;
}
}
@@ -1303,32 +1301,32 @@
}
/**
- * Registers a {@link HighTextContrastChangeListener} for changes in
- * the global high text contrast state of the system.
+ * Registers a {@link HighContrastTextStateChangeListener} for changes in
+ * the global high contrast text state of the system.
*
- * @param listener The listener.
- *
- * @hide
+ * @param executor a executor to call the listener from
+ * @param listener The listener to be called
*/
- public void addHighTextContrastStateChangeListener(
- @NonNull HighTextContrastChangeListener listener, @Nullable Handler handler) {
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void addHighContrastTextStateChangeListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull HighContrastTextStateChangeListener listener
+ ) {
synchronized (mLock) {
- mHighTextContrastStateChangeListeners
- .put(listener, (handler == null) ? mHandler : handler);
+ mHighContrastTextStateChangeListeners.put(listener, executor);
}
}
/**
- * Unregisters a {@link HighTextContrastChangeListener}.
+ * Unregisters a {@link HighContrastTextStateChangeListener}.
*
* @param listener The listener.
- *
- * @hide
*/
- public void removeHighTextContrastStateChangeListener(
- @NonNull HighTextContrastChangeListener listener) {
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void removeHighContrastTextStateChangeListener(
+ @NonNull HighContrastTextStateChangeListener listener) {
synchronized (mLock) {
- mHighTextContrastStateChangeListeners.remove(listener);
+ mHighContrastTextStateChangeListeners.remove(listener);
}
}
@@ -1505,13 +1503,13 @@
final boolean wasEnabled = isEnabled();
final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
- final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
+ final boolean wasHighTextContrastEnabled = mIsHighContrastTextEnabled;
final boolean wasAudioDescriptionByDefaultRequested = mIsAudioDescriptionByDefaultRequested;
// Ensure listeners get current state from isZzzEnabled() calls.
mIsEnabled = enabled;
mIsTouchExplorationEnabled = touchExplorationEnabled;
- mIsHighTextContrastEnabled = highTextContrastEnabled;
+ mIsHighContrastTextEnabled = highTextContrastEnabled;
mIsAudioDescriptionByDefaultRequested = audioDescriptionEnabled;
if (wasEnabled != isEnabled()) {
@@ -1523,7 +1521,7 @@
}
if (wasHighTextContrastEnabled != highTextContrastEnabled) {
- notifyHighTextContrastStateChanged();
+ notifyHighContrastTextStateChanged();
}
if (wasAudioDescriptionByDefaultRequested
@@ -2397,24 +2395,24 @@
}
/**
- * Notifies the registered {@link HighTextContrastChangeListener}s.
+ * Notifies the registered {@link HighContrastTextStateChangeListener}s.
*/
- private void notifyHighTextContrastStateChanged() {
+ private void notifyHighContrastTextStateChanged() {
final boolean isHighTextContrastEnabled;
- final ArrayMap<HighTextContrastChangeListener, Handler> listeners;
+ final ArrayMap<HighContrastTextStateChangeListener, Executor> listeners;
synchronized (mLock) {
- if (mHighTextContrastStateChangeListeners.isEmpty()) {
+ if (mHighContrastTextStateChangeListeners.isEmpty()) {
return;
}
- isHighTextContrastEnabled = mIsHighTextContrastEnabled;
- listeners = new ArrayMap<>(mHighTextContrastStateChangeListeners);
+ isHighTextContrastEnabled = mIsHighContrastTextEnabled;
+ listeners = new ArrayMap<>(mHighContrastTextStateChangeListeners);
}
final int numListeners = listeners.size();
for (int i = 0; i < numListeners; i++) {
- final HighTextContrastChangeListener listener = listeners.keyAt(i);
- listeners.valueAt(i).post(() ->
- listener.onHighTextContrastStateChanged(isHighTextContrastEnabled));
+ final HighContrastTextStateChangeListener listener = listeners.keyAt(i);
+ listeners.valueAt(i).execute(() ->
+ listener.onHighContrastTextStateChanged(isHighTextContrastEnabled));
}
}
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 513587e..b9e9750 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -4,6 +4,13 @@
# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
flag {
+ name: "a11y_expansion_state_api"
+ namespace: "accessibility"
+ description: "Enables new APIs for an app to convey if a node is expanded or collapsed."
+ bug: "362782536"
+}
+
+flag {
name: "a11y_overlay_callbacks"
is_exported: true
namespace: "accessibility"
diff --git a/core/java/android/view/autofill/AutofillStateFingerprint.java b/core/java/android/view/autofill/AutofillStateFingerprint.java
index 2db4285..7f3858e 100644
--- a/core/java/android/view/autofill/AutofillStateFingerprint.java
+++ b/core/java/android/view/autofill/AutofillStateFingerprint.java
@@ -97,7 +97,6 @@
if (sDebug) {
Log.d(TAG, "Autofillable views count prior to auth:" + autofillableViews.size());
}
-// ArrayList<Integer> hashes = getFingerprintIds(autofillableViews);
ArrayMap<Integer, View> hashes = getFingerprintIds(autofillableViews);
for (Map.Entry<Integer, View> entry : hashes.entrySet()) {
@@ -123,7 +122,6 @@
if (view != null) {
int id = getEphemeralFingerprintId(view, 0 /* position irrelevant */);
AutofillId autofillId = view.getAutofillId();
- autofillId.setSessionId(mSessionId);
mHashToAutofillIdMap.put(id, autofillId);
} else {
if (sDebug) {
diff --git a/core/java/android/window/flags/DesktopModeFlags.java b/core/java/android/window/flags/DesktopModeFlags.java
index 944a106..47af50da 100644
--- a/core/java/android/window/flags/DesktopModeFlags.java
+++ b/core/java/android/window/flags/DesktopModeFlags.java
@@ -64,7 +64,8 @@
ENABLE_WINDOWING_EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(
Flags::enableDesktopWindowingTaskbarRunningApps, true),
- ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false);
+ ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false),
+ ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false);
private static final String TAG = "DesktopModeFlagsUtil";
// Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index fbc30ed..b22aa22 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -162,6 +162,13 @@
}
flag {
+ name: "enable_a11y_metrics"
+ namespace: "lse_desktop_experience"
+ description: "Whether to enable log collection for a11y actions in desktop windowing mode"
+ bug: "341319597"
+}
+
+flag {
name: "enable_caption_compat_inset_force_consumption"
namespace: "lse_desktop_experience"
description: "Enables force-consumption of caption bar insets for immersive apps in freeform"
@@ -306,3 +313,10 @@
description: "Allow entering desktop mode by default on freeform displays"
bug: "361419732"
}
+
+flag {
+ name: "enable_desktop_app_launch_alttab_transitions"
+ namespace: "lse_desktop_experience"
+ description: "Enables custom transitions for alt-tab app launches in Desktop Mode."
+ bug: "370735595"
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index fef5e83..4aebde5 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -26,6 +26,7 @@
import android.os.RemoteException;
import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Arrays;
@@ -37,38 +38,40 @@
* @deprecated Legacy system channel, which is no longer used,
*/
@Deprecated public static String VIRTUAL_KEYBOARD = "VIRTUAL_KEYBOARD";
- public static String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD";
- public static String SECURITY = "SECURITY";
- public static String CAR_MODE = "CAR_MODE";
- public static String ACCOUNT = "ACCOUNT";
- public static String DEVELOPER = "DEVELOPER";
- public static String DEVELOPER_IMPORTANT = "DEVELOPER_IMPORTANT";
- public static String UPDATES = "UPDATES";
- public static String NETWORK_STATUS = "NETWORK_STATUS";
- public static String NETWORK_ALERTS = "NETWORK_ALERTS";
- public static String NETWORK_AVAILABLE = "NETWORK_AVAILABLE";
- public static String VPN = "VPN";
+ public static final String PHYSICAL_KEYBOARD = "PHYSICAL_KEYBOARD";
+ public static final String SECURITY = "SECURITY";
+ public static final String CAR_MODE = "CAR_MODE";
+ public static final String ACCOUNT = "ACCOUNT";
+ public static final String DEVELOPER = "DEVELOPER";
+ public static final String DEVELOPER_IMPORTANT = "DEVELOPER_IMPORTANT";
+ public static final String UPDATES = "UPDATES";
+ public static final String NETWORK_STATUS = "NETWORK_STATUS";
+ public static final String NETWORK_ALERTS = "NETWORK_ALERTS";
+ public static final String NETWORK_AVAILABLE = "NETWORK_AVAILABLE";
+ public static final String VPN = "VPN";
/**
* @deprecated Legacy device admin channel with low importance which is no longer used,
* Use the high importance {@link #DEVICE_ADMIN} channel instead.
*/
- @Deprecated public static String DEVICE_ADMIN_DEPRECATED = "DEVICE_ADMIN";
- public static String DEVICE_ADMIN = "DEVICE_ADMIN_ALERTS";
- public static String ALERTS = "ALERTS";
- public static String RETAIL_MODE = "RETAIL_MODE";
- public static String USB = "USB";
- public static String FOREGROUND_SERVICE = "FOREGROUND_SERVICE";
- public static String HEAVY_WEIGHT_APP = "HEAVY_WEIGHT_APP";
+ @Deprecated public static final String DEVICE_ADMIN_DEPRECATED = "DEVICE_ADMIN";
+ public static final String DEVICE_ADMIN = "DEVICE_ADMIN_ALERTS";
+ public static final String ALERTS = "ALERTS";
+ public static final String RETAIL_MODE = "RETAIL_MODE";
+ public static final String USB = "USB";
+ public static final String FOREGROUND_SERVICE = "FOREGROUND_SERVICE";
+ public static final String HEAVY_WEIGHT_APP = "HEAVY_WEIGHT_APP";
/**
* @deprecated Legacy system changes channel with low importance which is no longer used,
* Use the default importance {@link #SYSTEM_CHANGES} channel instead.
*/
- @Deprecated public static String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES";
- public static String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS";
- public static String DO_NOT_DISTURB = "DO_NOT_DISTURB";
- public static String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION";
- public static String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY";
- public static String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS";
+ @Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES";
+ public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS";
+ public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION";
+ public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY";
+ public static final String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS";
+
+ @VisibleForTesting
+ static final String OBSOLETE_DO_NOT_DISTURB = "DO_NOT_DISTURB";
public static void createAll(Context context) {
final NotificationManager nm = context.getSystemService(NotificationManager.class);
@@ -193,11 +196,6 @@
.build());
channelsList.add(systemChanges);
- NotificationChannel dndChanges = new NotificationChannel(DO_NOT_DISTURB,
- context.getString(R.string.notification_channel_do_not_disturb),
- NotificationManager.IMPORTANCE_LOW);
- channelsList.add(dndChanges);
-
final NotificationChannel newFeaturePrompt = new NotificationChannel(
ACCESSIBILITY_MAGNIFICATION,
context.getString(R.string.notification_channel_accessibility_magnification),
@@ -218,6 +216,9 @@
channelsList.add(abusiveBackgroundAppsChannel);
nm.createNotificationChannels(channelsList);
+
+ // Delete channels created by previous Android versions that are no longer used.
+ nm.deleteNotificationChannel(OBSOLETE_DO_NOT_DISTURB);
}
private static String getDeviceAdminNotificationChannelName(Context context) {
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 4d0cd27..f3dc896 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -56,6 +56,7 @@
import android.tracing.perfetto.Producer;
import android.tracing.perfetto.TracingContext;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import android.util.LongArray;
import android.util.Slog;
@@ -351,6 +352,10 @@
}
private void registerGroupsLocally(@NonNull IProtoLogGroup[] protoLogGroups) {
+ // Verify we don't have id collisions, if we do we want to know as soon as possible and
+ // we might want to manually specify an id for the group with a collision
+ verifyNoCollisionsOrDuplicates(protoLogGroups);
+
final var groupsLoggingToLogcat = new ArrayList<String>();
for (IProtoLogGroup protoLogGroup : protoLogGroups) {
mLogGroups.put(protoLogGroup.name(), protoLogGroup);
@@ -369,6 +374,19 @@
}
}
+ private void verifyNoCollisionsOrDuplicates(@NonNull IProtoLogGroup[] protoLogGroups) {
+ final var groupId = new ArraySet<Integer>();
+
+ for (IProtoLogGroup protoLogGroup : protoLogGroups) {
+ if (groupId.contains(protoLogGroup.getId())) {
+ throw new RuntimeException(
+ "Group with same id (" + protoLogGroup.getId() + ") registered twice. "
+ + "Potential duplicate or hash id collision.");
+ }
+ groupId.add(protoLogGroup.getId());
+ }
+ }
+
/**
* Responds to a shell command.
*/
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index adf03fe..60213b1 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -22,8 +22,8 @@
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogLevel;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
/**
* ProtoLog API - exposes static logging methods. Usage of this API is similar
@@ -73,7 +73,7 @@
if (sProtoLogInstance != null) {
// The ProtoLog instance has already been initialized in this process
final var alreadyRegisteredGroups = sProtoLogInstance.getRegisteredGroups();
- final var allGroups = new ArrayList<>(alreadyRegisteredGroups);
+ final var allGroups = new HashSet<>(alreadyRegisteredGroups);
allGroups.addAll(Arrays.stream(groups).toList());
groups = allGroups.toArray(new IProtoLogGroup[0]);
}
diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java
new file mode 100644
index 0000000..6521870
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java
@@ -0,0 +1,93 @@
+/*
+ * 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.internal.protolog;
+
+import android.annotation.NonNull;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+
+public class ProtoLogGroup implements IProtoLogGroup {
+
+ /** The name should be unique across the codebase. */
+ @NonNull
+ private final String mName;
+ @NonNull
+ private final String mTag;
+ private final boolean mEnabled;
+ private boolean mLogToProto;
+ private boolean mLogToLogcat;
+
+ public ProtoLogGroup(@NonNull String name) {
+ this(name, name);
+ }
+
+ public ProtoLogGroup(@NonNull String name, @NonNull String tag) {
+ this(name, tag, true);
+ }
+
+ public ProtoLogGroup(@NonNull String name, @NonNull String tag, boolean enabled) {
+ mName = name;
+ mTag = tag;
+ mEnabled = enabled;
+ mLogToProto = enabled;
+ mLogToLogcat = enabled;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Deprecated
+ @Override
+ public boolean isLogToProto() {
+ return mLogToProto;
+ }
+
+ @Override
+ public boolean isLogToLogcat() {
+ return mLogToLogcat;
+ }
+
+ @Override
+ @NonNull
+ public String getTag() {
+ return mTag;
+ }
+
+ @Deprecated
+ @Override
+ public void setLogToProto(boolean logToProto) {
+ mLogToProto = logToProto;
+ }
+
+ @Override
+ public void setLogToLogcat(boolean logToLogcat) {
+ mLogToLogcat = logToLogcat;
+ }
+
+ @Override
+ @NonNull
+ public String name() {
+ return mName;
+ }
+
+ @Override
+ public int getId() {
+ return mName.hashCode();
+ }
+}
diff --git a/core/java/com/android/internal/protolog/TEST_MAPPING b/core/java/com/android/internal/protolog/TEST_MAPPING
index 37d57ee..b51d19d 100644
--- a/core/java/com/android/internal/protolog/TEST_MAPPING
+++ b/core/java/com/android/internal/protolog/TEST_MAPPING
@@ -1,6 +1,9 @@
{
"postsubmit": [
{
+ "name": "TracingTests"
+ },
+ {
"name": "ProtologPerfTests"
}
]
diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp
index 3370f38..ba7e705 100644
--- a/core/jni/android_database_SQLiteConnection.cpp
+++ b/core/jni/android_database_SQLiteConnection.cpp
@@ -16,27 +16,22 @@
#define LOG_TAG "SQLiteConnection"
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
+#include <android-base/mapped_file.h>
#include <android_runtime/AndroidRuntime.h>
#include <android_runtime/Log.h>
-
-#include <utils/Log.h>
-#include <utils/String8.h>
-#include <utils/String16.h>
-#include <cutils/ashmem.h>
-#include <sys/mman.h>
-
-#include <string.h>
-#include <unistd.h>
-
#include <androidfw/CursorWindow.h>
-
+#include <cutils/ashmem.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
#include <sqlite3.h>
#include <sqlite3_android.h>
+#include <string.h>
+#include <unistd.h>
+#include <utils/Log.h>
+#include <utils/String16.h>
+#include <utils/String8.h>
#include "android_database_SQLiteCommon.h"
-
#include "core_jni_helpers.h"
// Set to 1 to use UTF16 storage for localized indexes.
@@ -669,13 +664,14 @@
ALOGE("ashmem_create_region failed: %s", strerror(error));
} else {
if (length > 0) {
- void* ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- if (ptr == MAP_FAILED) {
+ std::unique_ptr<base::MappedFile> mappedFile =
+ base::MappedFile::FromFd(fd, 0, length, PROT_READ | PROT_WRITE);
+ if (mappedFile == nullptr) {
error = errno;
ALOGE("mmap failed: %s", strerror(error));
} else {
- memcpy(ptr, data, length);
- munmap(ptr, length);
+ memcpy(mappedFile->data(), data, length);
+ mappedFile.reset();
}
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d35c66e..ed33ede 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8449,6 +8449,29 @@
<permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
android:protectionLevel="signature"/>
+ <!-- @SystemApi
+ @FlaggedApi("android.content.pm.verification_service")
+ Allows app to be the verification agent to verify packages.
+ <p>Protection level: signature|privileged
+ @hide
+ -->
+ <permission android:name="android.permission.VERIFICATION_AGENT"
+ android:protectionLevel="signature|privileged"
+ android:featureFlag="android.content.pm.verification_service" />
+
+ <!-- @SystemApi
+ @FlaggedApi("android.content.pm.verification_service")
+ Must be required by a privileged {@link android.content.pm.verify.pkg.VerifierService}
+ to ensure that only the system can bind to it.
+ This permission should not be held by anything other than the system.
+ <p>Not for use by third-party applications. </p>
+ <p>Protection level: signature
+ @hide
+ -->
+ <permission android:name="android.permission.BIND_VERIFICATION_AGENT"
+ android:protectionLevel="internal"
+ android:featureFlag="android.content.pm.verification_service" />
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5c0dca2..1a3a30d 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1250,6 +1250,7 @@
a watch, setting this config is no-op.
0 - Nothing
1 - Switch to the recent app
+ 2 - Launch the default fitness app
-->
<integer name="config_doublePressOnStemPrimaryBehavior">0</integer>
@@ -2309,10 +2310,6 @@
spatial audio is enabled for a newly connected audio device -->
<bool name="config_spatial_audio_head_tracking_enabled_default">false</bool>
- <!-- Flag indicating whether platform level volume adjustments are enabled for remote sessions
- on grouped devices. -->
- <bool name="config_volumeAdjustmentForRemoteGroupSessions">true</bool>
-
<!-- Flag indicating current media Output Switcher version. -->
<integer name="config_mediaOutputSwitchDialogVersion">1</integer>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d634210..7aca535 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5818,16 +5818,6 @@
<!-- Title for the notification channel notifying user of settings system changes. [CHAR LIMIT=NONE] -->
<string name="notification_channel_system_changes">System changes</string>
- <!-- Title for the notification channel notifying user of do not disturb system changes (i.e. Do Not Disturb has changed). [CHAR LIMIT=NONE] -->
- <string name="notification_channel_do_not_disturb">Do Not Disturb</string>
- <!-- Title of notification indicating do not disturb visual interruption settings have changed when upgrading to P -->
- <string name="zen_upgrade_notification_visd_title">New: Do Not Disturb is hiding notifications</string>
- <!-- Content of notification indicating users can tap on the notification to go to dnd behavior settings -->
- <string name="zen_upgrade_notification_visd_content">Tap to learn more and change.</string>
- <!-- Title of notification indicating do not disturb settings have changed when upgrading to P -->
- <string name="zen_upgrade_notification_title">Do Not Disturb has changed</string>
- <!-- Content of notification indicating users can tap on the notification to go to dnd behavior settings -->
- <string name="zen_upgrade_notification_content">Tap to check what\'s blocked.</string>
<!-- Notification permission informational notification text -->
<!-- Title for notification inviting users to review their notification settings [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 807df1b..d5298ac 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3937,7 +3937,6 @@
<java-symbol type="string" name="notification_channel_usb" />
<java-symbol type="string" name="notification_channel_heavy_weight_app" />
<java-symbol type="string" name="notification_channel_system_changes" />
- <java-symbol type="string" name="notification_channel_do_not_disturb" />
<java-symbol type="string" name="notification_channel_accessibility_magnification" />
<java-symbol type="string" name="notification_channel_accessibility_security_policy" />
<java-symbol type="string" name="notification_channel_display" />
@@ -4164,11 +4163,6 @@
<!-- For Wear devices -->
<java-symbol type="array" name="config_wearActivityModeRadios" />
- <java-symbol type="string" name="zen_upgrade_notification_title" />
- <java-symbol type="string" name="zen_upgrade_notification_content" />
- <java-symbol type="string" name="zen_upgrade_notification_visd_title" />
- <java-symbol type="string" name="zen_upgrade_notification_visd_content" />
-
<java-symbol type="string" name="review_notification_settings_title" />
<java-symbol type="string" name="review_notification_settings_text" />
<java-symbol type="string" name="review_notification_settings_remind_me_action" />
@@ -5102,8 +5096,6 @@
<java-symbol type="dimen" name="config_wallpaperDimAmount" />
- <java-symbol type="bool" name="config_volumeAdjustmentForRemoteGroupSessions" />
-
<java-symbol type="integer" name="config_mediaOutputSwitchDialogVersion" />
<!-- List of shared library packages that should be loaded by the classloader after the
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 9821d43..56e18e6 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -250,7 +250,7 @@
"androidx.test.rules",
"androidx.test.ext.junit",
"androidx.test.uiautomator_uiautomator",
- "compatibility-device-util-axt",
+ "compatibility-device-util-axt-ravenwood",
"flag-junit",
"platform-test-annotations",
"flag-junit",
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
new file mode 100644
index 0000000..987f68d
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.VersionedPackage;
+import android.content.pm.verify.pkg.IVerificationSessionCallback;
+import android.content.pm.verify.pkg.IVerificationSessionInterface;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationSessionTest {
+ private static final int TEST_ID = 100;
+ private static final int TEST_INSTALL_SESSION_ID = 33;
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+ private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 =
+ new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME,
+ Collections.singletonList("path1"), "sharedLib1", 101,
+ SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1),
+ null, null, false);
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 =
+ new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME,
+ Collections.singletonList("path2"), "sharedLib2", 102,
+ SharedLibraryInfo.TYPE_DYNAMIC,
+ new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false);
+ private static final long TEST_TIMEOUT_TIME = System.currentTimeMillis();
+ private static final long TEST_EXTEND_TIME = 2000L;
+ private static final String TEST_KEY = "test key";
+ private static final String TEST_VALUE = "test value";
+
+ private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
+ private final PersistableBundle mTestExtensionParams = new PersistableBundle();
+ @Mock
+ private IVerificationSessionInterface mTestSessionInterface;
+ @Mock
+ private IVerificationSessionCallback mTestCallback;
+ private VerificationSession mTestSession;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1);
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2);
+ mTestExtensionParams.putString(TEST_KEY, TEST_VALUE);
+ mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
+ TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries,
+ mTestExtensionParams, mTestSessionInterface, mTestCallback);
+ }
+
+ @Test
+ public void testParcel() {
+ Parcel parcel = Parcel.obtain();
+ mTestSession.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ VerificationSession sessionFromParcel =
+ VerificationSession.CREATOR.createFromParcel(parcel);
+ assertThat(sessionFromParcel.getId()).isEqualTo(TEST_ID);
+ assertThat(sessionFromParcel.getInstallSessionId()).isEqualTo(TEST_INSTALL_SESSION_ID);
+ assertThat(sessionFromParcel.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(sessionFromParcel.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+ assertThat(sessionFromParcel.getSigningInfo().getSigningDetails())
+ .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+ List<SharedLibraryInfo> declaredLibrariesFromParcel =
+ sessionFromParcel.getDeclaredLibraries();
+ assertThat(declaredLibrariesFromParcel).hasSize(2);
+ // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+ assertThat(declaredLibrariesFromParcel.getFirst().toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+ assertThat(declaredLibrariesFromParcel.get(1).toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+ // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+ // structure is different, but all the key/value pairs should be preserved as before.
+ assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY))
+ .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ }
+
+ @Test
+ public void testInterface() throws Exception {
+ when(mTestSessionInterface.getTimeoutTime(anyInt())).thenAnswer(i -> TEST_TIMEOUT_TIME);
+ when(mTestSessionInterface.extendTimeRemaining(anyInt(), anyLong())).thenAnswer(
+ i -> i.getArguments()[1]);
+
+ assertThat(mTestSession.getTimeoutTime()).isEqualTo(TEST_TIMEOUT_TIME);
+ verify(mTestSessionInterface, times(1)).getTimeoutTime(eq(TEST_ID));
+ assertThat(mTestSession.extendTimeRemaining(TEST_EXTEND_TIME)).isEqualTo(TEST_EXTEND_TIME);
+ verify(mTestSessionInterface, times(1)).extendTimeRemaining(
+ eq(TEST_ID), eq(TEST_EXTEND_TIME));
+ }
+
+ @Test
+ public void testCallback() throws Exception {
+ PersistableBundle response = new PersistableBundle();
+ response.putString("test key", "test value");
+ final VerificationStatus status =
+ new VerificationStatus.Builder().setVerified(true).build();
+ mTestSession.reportVerificationComplete(status);
+ verify(mTestCallback, times(1)).reportVerificationComplete(
+ eq(TEST_ID), eq(status));
+ mTestSession.reportVerificationComplete(status, response);
+ verify(mTestCallback, times(1))
+ .reportVerificationCompleteWithExtensionResponse(
+ eq(TEST_ID), eq(status), eq(response));
+
+ final int reason = VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN;
+ mTestSession.reportVerificationIncomplete(reason);
+ verify(mTestCallback, times(1)).reportVerificationIncomplete(
+ eq(TEST_ID), eq(reason));
+ }
+}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java
new file mode 100644
index 0000000..67d407a
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerificationStatusTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationStatusTest {
+ private static final boolean TEST_VERIFIED = true;
+ private static final int TEST_ASL_STATUS = VerificationStatus.VERIFIER_STATUS_ASL_GOOD;
+ private static final String TEST_FAILURE_MESSAGE = "test test";
+ private static final String TEST_KEY = "test key";
+ private static final String TEST_VALUE = "test value";
+ private final PersistableBundle mTestExtras = new PersistableBundle();
+ private VerificationStatus mStatus;
+
+ @Before
+ public void setUpWithBuilder() {
+ mTestExtras.putString(TEST_KEY, TEST_VALUE);
+ mStatus = new VerificationStatus.Builder()
+ .setAslStatus(TEST_ASL_STATUS)
+ .setFailureMessage(TEST_FAILURE_MESSAGE)
+ .setVerified(TEST_VERIFIED)
+ .build();
+ }
+
+ @Test
+ public void testGetters() {
+ assertThat(mStatus.isVerified()).isEqualTo(TEST_VERIFIED);
+ assertThat(mStatus.getAslStatus()).isEqualTo(TEST_ASL_STATUS);
+ assertThat(mStatus.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE);
+ }
+
+ @Test
+ public void testParcel() {
+ Parcel parcel = Parcel.obtain();
+ mStatus.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ VerificationStatus statusFromParcel = VerificationStatus.CREATOR.createFromParcel(parcel);
+ assertThat(statusFromParcel.isVerified()).isEqualTo(TEST_VERIFIED);
+ assertThat(statusFromParcel.getAslStatus()).isEqualTo(TEST_ASL_STATUS);
+ assertThat(statusFromParcel.getFailureMessage()).isEqualTo(TEST_FAILURE_MESSAGE);
+ }
+}
diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
new file mode 100644
index 0000000..7f73a1e
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.SigningInfo;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerifierService;
+import android.net.Uri;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerifierServiceTest {
+ private static final int TEST_ID = 100;
+ private static final int TEST_INSTALL_SESSION_ID = 33;
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+ private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private VerifierService mService;
+ private VerificationSession mSession;
+
+ @Before
+ public void setUp() {
+ mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS);
+ mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID,
+ TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO,
+ new ArrayList<>(),
+ new PersistableBundle(), null, null);
+ }
+
+ @Test
+ public void testBind() throws Exception {
+ Intent intent = Mockito.mock(Intent.class);
+ when(intent.getAction()).thenReturn(PackageManager.ACTION_VERIFY_PACKAGE);
+ IVerifierService binder =
+ (IVerifierService) mService.onBind(intent);
+ assertThat(binder).isNotNull();
+ binder.onPackageNameAvailable(TEST_PACKAGE_NAME);
+ verify(mService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME));
+ binder.onVerificationCancelled(TEST_PACKAGE_NAME);
+ verify(mService).onVerificationCancelled(eq(TEST_PACKAGE_NAME));
+ binder.onVerificationRequired(mSession);
+ verify(mService).onVerificationRequired(eq(mSession));
+ binder.onVerificationRetry(mSession);
+ verify(mService).onVerificationRetry(eq(mSession));
+ binder.onVerificationTimeout(TEST_ID);
+ verify(mService).onVerificationTimeout(eq(TEST_ID));
+ }
+
+ @Test
+ public void testBindFailsWithWrongIntent() {
+ Intent intent = Mockito.mock(Intent.class);
+ when(intent.getAction()).thenReturn(Intent.ACTION_SEND);
+ assertThat(mService.onBind(intent)).isNull();
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
new file mode 100644
index 0000000..0bf406c
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/notification/SystemNotificationChannelsTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.internal.notification;
+
+import static com.android.internal.notification.SystemNotificationChannels.ABUSIVE_BACKGROUND_APPS;
+import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_MAGNIFICATION;
+import static com.android.internal.notification.SystemNotificationChannels.ACCESSIBILITY_SECURITY_POLICY;
+import static com.android.internal.notification.SystemNotificationChannels.ACCOUNT;
+import static com.android.internal.notification.SystemNotificationChannels.ALERTS;
+import static com.android.internal.notification.SystemNotificationChannels.CAR_MODE;
+import static com.android.internal.notification.SystemNotificationChannels.DEVELOPER;
+import static com.android.internal.notification.SystemNotificationChannels.DEVELOPER_IMPORTANT;
+import static com.android.internal.notification.SystemNotificationChannels.DEVICE_ADMIN;
+import static com.android.internal.notification.SystemNotificationChannels.FOREGROUND_SERVICE;
+import static com.android.internal.notification.SystemNotificationChannels.HEAVY_WEIGHT_APP;
+import static com.android.internal.notification.SystemNotificationChannels.NETWORK_ALERTS;
+import static com.android.internal.notification.SystemNotificationChannels.NETWORK_AVAILABLE;
+import static com.android.internal.notification.SystemNotificationChannels.NETWORK_STATUS;
+import static com.android.internal.notification.SystemNotificationChannels.OBSOLETE_DO_NOT_DISTURB;
+import static com.android.internal.notification.SystemNotificationChannels.PHYSICAL_KEYBOARD;
+import static com.android.internal.notification.SystemNotificationChannels.RETAIL_MODE;
+import static com.android.internal.notification.SystemNotificationChannels.SECURITY;
+import static com.android.internal.notification.SystemNotificationChannels.SYSTEM_CHANGES;
+import static com.android.internal.notification.SystemNotificationChannels.UPDATES;
+import static com.android.internal.notification.SystemNotificationChannels.USB;
+import static com.android.internal.notification.SystemNotificationChannels.VPN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.testing.TestableContext;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class SystemNotificationChannelsTest {
+
+ @Rule public TestableContext mContext = new TestableContext(
+ ApplicationProvider.getApplicationContext());
+
+ @Mock private NotificationManager mNm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext.addMockSystemService(NotificationManager.class, mNm);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void createAll_createsExpectedChannels() {
+ ArgumentCaptor<List<NotificationChannel>> createdChannelsCaptor =
+ ArgumentCaptor.forClass(List.class);
+
+ SystemNotificationChannels.createAll(mContext);
+
+ verify(mNm).createNotificationChannels(createdChannelsCaptor.capture());
+ List<NotificationChannel> createdChannels = createdChannelsCaptor.getValue();
+ assertThat(createdChannels.stream().map(NotificationChannel::getId).toList())
+ .containsExactly(PHYSICAL_KEYBOARD, SECURITY, CAR_MODE, ACCOUNT, DEVELOPER,
+ DEVELOPER_IMPORTANT, UPDATES, NETWORK_STATUS, NETWORK_ALERTS,
+ NETWORK_AVAILABLE, VPN, DEVICE_ADMIN, ALERTS, RETAIL_MODE, USB,
+ FOREGROUND_SERVICE, HEAVY_WEIGHT_APP, SYSTEM_CHANGES,
+ ACCESSIBILITY_MAGNIFICATION, ACCESSIBILITY_SECURITY_POLICY,
+ ABUSIVE_BACKGROUND_APPS);
+ }
+
+ @Test
+ public void createAll_deletesObsoleteChannels() {
+ ArgumentCaptor<String> deletedChannelCaptor = ArgumentCaptor.forClass(String.class);
+
+ SystemNotificationChannels.createAll(mContext);
+
+ verify(mNm, atLeastOnce()).deleteNotificationChannel(deletedChannelCaptor.capture());
+ List<String> deletedChannels = deletedChannelCaptor.getAllValues();
+ assertThat(deletedChannels).containsExactly(OBSOLETE_DO_NOT_DISTURB);
+ }
+}
diff --git a/core/tests/utiltests/Android.bp b/core/tests/utiltests/Android.bp
index cdc8a9e..7cf49ab 100644
--- a/core/tests/utiltests/Android.bp
+++ b/core/tests/utiltests/Android.bp
@@ -61,7 +61,7 @@
"androidx.annotation_annotation",
"androidx.test.rules",
"frameworks-base-testutils",
- "servicestests-utils",
+ "servicestests-utils-ravenwood",
],
srcs: [
"src/android/util/IRemoteMemoryIntArray.aidl",
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index 0b3e545..28c2ca3 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -155,7 +155,7 @@
/**
* Indicates whether this Canvas is drawing high contrast text.
*
- * @see android.view.accessibility.AccessibilityManager#isHighTextContrastEnabled()
+ * @see android.view.accessibility.AccessibilityManager#isHighContrastTextEnabled()
* @return True if high contrast text is enabled, false otherwise.
*
* @hide
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index b866382..68d8ebb 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -41,6 +41,7 @@
import android.text.TextUtils;
import com.android.internal.annotations.GuardedBy;
+import com.android.text.flags.Flags;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -2000,6 +2001,14 @@
}
/**
+ * A change ID for new font variation settings management.
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = 36)
+ public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L;
+
+ /**
* Sets TrueType or OpenType font variation settings. The settings string is constructed from
* multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
* and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
@@ -2028,12 +2037,16 @@
* </li>
* </ul>
*
+ * <p>Note: If the application that targets API 35 or before, this function mutates the
+ * underlying typeface instance.
+ *
* @param fontVariationSettings font variation settings. You can pass null or empty string as
* no variation settings.
*
- * @return true if the given settings is effective to at least one font file underlying this
- * typeface. This function also returns true for empty settings string. Otherwise
- * returns false
+ * @return If the application that targets API 36 or later and is running on devices API 36 or
+ * later, this function always returns true. Otherwise, this function returns true if
+ * the given settings is effective to at least one font file underlying this typeface.
+ * This function also returns true for empty settings string. Otherwise returns false.
*
* @throws IllegalArgumentException If given string is not a valid font variation settings
* format
@@ -2042,6 +2055,26 @@
* @see FontVariationAxis
*/
public boolean setFontVariationSettings(String fontVariationSettings) {
+ final boolean useFontVariationStore = Flags.typefaceRedesign()
+ && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT);
+ if (useFontVariationStore) {
+ FontVariationAxis[] axes =
+ FontVariationAxis.fromFontVariationSettings(fontVariationSettings);
+ if (axes == null) {
+ nSetFontVariationOverride(mNativePaint, 0);
+ mFontVariationSettings = null;
+ return true;
+ }
+
+ long builderPtr = nCreateFontVariationBuilder(axes.length);
+ for (int i = 0; i < axes.length; ++i) {
+ nAddFontVariationToBuilder(builderPtr, axes[i].getOpenTypeTagValue(),
+ axes[i].getStyleValue());
+ }
+ nSetFontVariationOverride(mNativePaint, builderPtr);
+ mFontVariationSettings = fontVariationSettings;
+ return true;
+ }
final String settings = TextUtils.nullIfEmpty(fontVariationSettings);
if (settings == mFontVariationSettings
|| (settings != null && settings.equals(mFontVariationSettings))) {
@@ -3829,7 +3862,12 @@
private static native void nSetTextSize(long paintPtr, float textSize);
@CriticalNative
private static native boolean nEqualsForTextMeasurement(long leftPaintPtr, long rightPaintPtr);
-
+ @CriticalNative
+ private static native long nCreateFontVariationBuilder(int size);
+ @CriticalNative
+ private static native void nAddFontVariationToBuilder(long builderPtr, int tag, float value);
+ @CriticalNative
+ private static native void nSetFontVariationOverride(long paintPtr, long builderPtr);
// Following Native methods are kept for old Robolectric JNI signature used by
// SystemUIGoogleRoboRNGTests
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index f857429..e493ed1 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -197,6 +197,7 @@
android_library {
name: "WindowManager-Shell",
srcs: [
+ "src/com/android/wm/shell/EventLogTags.logtags",
":wm_shell_protolog_src",
// TODO(b/168581922) protologtool do not support kotlin(*.kt)
":wm_shell-sources-kt",
@@ -220,6 +221,7 @@
"//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
"//frameworks/libs/systemui:iconloader_base",
"com_android_wm_shell_flags_lib",
+ "PlatformAnimationLib",
"WindowManager-Shell-proto",
"WindowManager-Shell-lite-proto",
"WindowManager-Shell-shared",
diff --git a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_immersive_exit_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_immersive_exit_button_dark.xml
new file mode 100644
index 0000000..5260450
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_immersive_exit_button_dark.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M240,840L240,720L120,720L120,640L320,640L320,840L240,840ZM640,840L640,640L840,640L840,720L720,720L720,840L640,840ZM120,320L120,240L240,240L240,120L320,120L320,320L120,320ZM640,320L640,120L720,120L720,240L840,240L840,320L640,320Z"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 766852d..d5b9703 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -97,6 +97,12 @@
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Camera issues?\nTap to refit"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Didn’t fix it?\nTap to revert"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"No camera issues? Tap to dismiss."</string>
+ <!-- no translation found for windowing_app_handle_education_tooltip (6398482412956375783) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (6285279585554484957) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_exit_education_tooltip (6685429075790085337) -->
+ <skip />
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"See and do more"</string>
<string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Drag in another app for split screen"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Double-tap outside an app to reposition it"</string>
@@ -129,7 +135,8 @@
<string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Open menu"</string>
<string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string>
<string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string>
- <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"This app can\'t be resized"</string>
+ <!-- no translation found for desktop_mode_non_resizable_snap_text (3771776422751387878) -->
+ <skip />
<string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string>
<string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string>
<string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index 75c445c..18048ff 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -97,6 +97,12 @@
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problemas com a câmera?\nToque para ajustar o enquadramento"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"O problema não foi corrigido?\nToque para reverter"</string>
<string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"Não tem problemas com a câmera? Toque para dispensar."</string>
+ <!-- no translation found for windowing_app_handle_education_tooltip (6398482412956375783) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_image_button_education_tooltip (6285279585554484957) -->
+ <skip />
+ <!-- no translation found for windowing_desktop_mode_exit_education_tooltip (6685429075790085337) -->
+ <skip />
<string name="letterbox_education_dialog_title" msgid="7739895354143295358">"Veja e faça mais"</string>
<string name="letterbox_education_split_screen_text" msgid="449233070804658627">"Arraste outro app para dividir a tela"</string>
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"Toque duas vezes fora de um app para reposicionar"</string>
@@ -126,15 +132,12 @@
<string name="manage_windows_text" msgid="5567366688493093920">"Gerenciar janelas"</string>
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
- <!-- no translation found for desktop_mode_app_header_chip_text (6366422614991687237) -->
- <skip />
+ <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Abrir o menu"</string>
<string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string>
<string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string>
- <string name="desktop_mode_non_resizable_snap_text" msgid="1049800446363800707">"Não é possível redimensionar o app"</string>
- <!-- no translation found for desktop_mode_maximize_menu_maximize_button_text (3090199175564175845) -->
+ <!-- no translation found for desktop_mode_non_resizable_snap_text (3771776422751387878) -->
<skip />
- <!-- no translation found for desktop_mode_maximize_menu_snap_left_button_text (8077452201179893424) -->
- <skip />
- <!-- no translation found for desktop_mode_maximize_menu_snap_right_button_text (7117751068945657304) -->
- <skip />
+ <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string>
+ <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar à esquerda"</string>
+ <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar à direita"</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/EventLogTags.logtags b/libs/WindowManager/Shell/src/com/android/wm/shell/EventLogTags.logtags
new file mode 100644
index 0000000..db960d1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/EventLogTags.logtags
@@ -0,0 +1,11 @@
+# See system/logging/logcat/event.logtags for a description of the format of this file.
+
+option java_package com.android.wm.shell
+
+# Do not change these names without updating the checkin_events setting in
+# google3/googledata/wireless/android/provisioning/gservices.config !!
+#
+
+38500 wm_shell_enter_desktop_mode (EnterReason|1|5),(SessionId|1|5)
+38501 wm_shell_exit_desktop_mode (ExitReason|1|5),(SessionId|1|5)
+38502 wm_shell_desktop_mode_task_update (TaskEvent|1|5),(InstanceId|1|5),(uid|1|5),(TaskHeight|1),(TaskWidth|1),(TaskX|1),(TaskY|1),(SessionId|1|5),(MinimiseReason|1|5),(UnminimiseReason|1|5),(VisibleTaskCount|1)
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 05ce361..71bcb59 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
@@ -20,10 +20,11 @@
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.net.Uri
-private val browserIntent = Intent()
+private val GenericBrowserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.parse("http:"))
@@ -32,9 +33,9 @@
* Returns a boolean indicating whether a given package is a browser app.
*/
fun isBrowserApp(context: Context, packageName: String, userId: Int): Boolean {
- browserIntent.setPackage(packageName)
+ GenericBrowserIntent.setPackage(packageName)
val list = context.packageManager.queryIntentActivitiesAsUser(
- browserIntent, PackageManager.MATCH_ALL, userId
+ GenericBrowserIntent, PackageManager.MATCH_ALL, userId
)
list.forEach {
@@ -44,3 +45,17 @@
}
return false
}
+
+/**
+ * Returns intent if there is a browser application available to handle the uri. Otherwise, returns
+ * null.
+ */
+fun getBrowserIntent(uri: Uri, packageManager: PackageManager): Intent? {
+ val intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER)
+ .setData(uri)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK)
+ // If there is no browser application available to handle intent, return null
+ val component = intent.resolveActivity(packageManager) ?: return null
+ intent.setComponent(component)
+ return intent
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 3e5adf3..5836085 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -1501,10 +1501,6 @@
int rootIdx = -1;
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change c = info.getChanges().get(i);
- if (c.hasFlags(FLAG_IS_WALLPAPER)) {
- st.setAlpha(c.getLeash(), 1.0f);
- continue;
- }
if (TransitionUtil.isOpeningMode(c.getMode())) {
final Point offset = c.getEndRelOffset();
st.setPosition(c.getLeash(), offset.x, offset.y);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 2c03059..7293956 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.dagger;
+import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT;
import android.annotation.Nullable;
@@ -61,8 +62,10 @@
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
+import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
@@ -87,6 +90,8 @@
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarterInitializer;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.PipTransitionController;
@@ -240,6 +245,7 @@
IWindowManager windowManager,
ShellCommandHandler shellCommandHandler,
ShellTaskOrganizer taskOrganizer,
+ @DynamicOverride DesktopModeTaskRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -266,6 +272,7 @@
shellCommandHandler,
windowManager,
taskOrganizer,
+ desktopRepository,
displayController,
shellController,
displayInsetsController,
@@ -330,9 +337,13 @@
static FreeformComponents provideFreeformComponents(
FreeformTaskListener taskListener,
FreeformTaskTransitionHandler transitionHandler,
- FreeformTaskTransitionObserver transitionObserver) {
+ FreeformTaskTransitionObserver transitionObserver,
+ FreeformTaskTransitionStarterInitializer transitionStarterInitializer) {
return new FreeformComponents(
- taskListener, Optional.of(transitionHandler), Optional.of(transitionObserver));
+ taskListener,
+ Optional.of(transitionHandler),
+ Optional.of(transitionObserver),
+ Optional.of(transitionStarterInitializer));
}
@WMSingleton
@@ -356,27 +367,15 @@
@WMSingleton
@Provides
static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
- ShellInit shellInit,
Transitions transitions,
- Context context,
- WindowDecorViewModel windowDecorViewModel,
DisplayController displayController,
@ShellMainThread ShellExecutor mainExecutor,
- @ShellAnimationThread ShellExecutor animExecutor,
- @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
- InteractionJankMonitor interactionJankMonitor,
- @ShellMainThread Handler handler) {
+ @ShellAnimationThread ShellExecutor animExecutor) {
return new FreeformTaskTransitionHandler(
- shellInit,
transitions,
- context,
- windowDecorViewModel,
displayController,
mainExecutor,
- animExecutor,
- desktopModeTaskRepository,
- interactionJankMonitor,
- handler);
+ animExecutor);
}
@WMSingleton
@@ -390,6 +389,23 @@
context, shellInit, transitions, windowDecorViewModel);
}
+ @WMSingleton
+ @Provides
+ static FreeformTaskTransitionStarterInitializer provideFreeformTaskTransitionStarterInitializer(
+ ShellInit shellInit,
+ WindowDecorViewModel windowDecorViewModel,
+ FreeformTaskTransitionHandler freeformTaskTransitionHandler,
+ Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler) {
+ FreeformTaskTransitionStarter transitionStarter;
+ if (desktopMixedTransitionHandler.isPresent()) {
+ transitionStarter = desktopMixedTransitionHandler.get();
+ } else {
+ transitionStarter = freeformTaskTransitionHandler;
+ }
+ return new FreeformTaskTransitionStarterInitializer(shellInit, windowDecorViewModel,
+ transitionStarter);
+ }
+
//
// One handed mode
//
@@ -699,7 +715,17 @@
InteractionJankMonitor interactionJankMonitor,
@ShellMainThread Handler handler) {
return new ExitDesktopTaskTransitionHandler(
- transitions, context, interactionJankMonitor, handler);
+ transitions, context, interactionJankMonitor, handler);
+ }
+
+ @WMSingleton
+ @Provides
+ static CloseDesktopTaskTransitionHandler provideCloseDesktopTaskTransitionHandler(
+ Context context,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellAnimationThread ShellExecutor animExecutor
+ ) {
+ return new CloseDesktopTaskTransitionHandler(context, mainExecutor, animExecutor);
}
@WMSingleton
@@ -758,6 +784,32 @@
@WMSingleton
@Provides
+ static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler(
+ Context context,
+ Transitions transitions,
+ @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ FreeformTaskTransitionHandler freeformTaskTransitionHandler,
+ CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler,
+ InteractionJankMonitor interactionJankMonitor,
+ @ShellMainThread Handler handler
+ ) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)
+ || !ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new DesktopMixedTransitionHandler(
+ context,
+ transitions,
+ desktopModeTaskRepository,
+ freeformTaskTransitionHandler,
+ closeDesktopTaskTransitionHandler,
+ interactionJankMonitor,
+ handler));
+ }
+
+ @WMSingleton
+ @Provides
static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
Context context,
ShellInit shellInit,
@@ -799,10 +851,11 @@
static DesktopWindowingEducationTooltipController
provideDesktopWindowingEducationTooltipController(
Context context,
- AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory
+ AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
+ DisplayController displayController
) {
return new DesktopWindowingEducationTooltipController(context,
- additionalSystemViewContainerFactory);
+ additionalSystemViewContainerFactory, displayController);
}
@OptIn(markerClass = ExperimentalCoroutinesApi.class)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt
new file mode 100644
index 0000000..a16c15df
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.RectEvaluator
+import android.animation.ValueAnimator
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.graphics.Rect
+import android.os.IBinder
+import android.util.TypedValue
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import androidx.core.animation.addListener
+import com.android.app.animation.Interpolators
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.transition.Transitions
+import java.util.function.Supplier
+
+/** The [Transitions.TransitionHandler] that handles transitions for closing desktop mode tasks. */
+class CloseDesktopTaskTransitionHandler
+@JvmOverloads
+constructor(
+ private val context: Context,
+ private val mainExecutor: ShellExecutor,
+ private val animExecutor: ShellExecutor,
+ private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
+) : Transitions.TransitionHandler {
+
+ private val runningAnimations = mutableMapOf<IBinder, List<Animator>>()
+
+ /** Returns null, as it only handles transitions started from Shell. */
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? = null
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ if (info.type != WindowManager.TRANSIT_CLOSE) return false
+ val animations = mutableListOf<Animator>()
+ val onAnimFinish: (Animator) -> Unit = { animator ->
+ mainExecutor.execute {
+ // Animation completed
+ animations.remove(animator)
+ if (animations.isEmpty()) {
+ // All animations completed, finish the transition
+ runningAnimations.remove(transition)
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ }
+ }
+ }
+ animations +=
+ info.changes
+ .filter {
+ it.mode == WindowManager.TRANSIT_CLOSE &&
+ it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+ .map { createCloseAnimation(it, finishTransaction, onAnimFinish) }
+ if (animations.isEmpty()) return false
+ runningAnimations[transition] = animations
+ animExecutor.execute { animations.forEach(Animator::start) }
+ return true
+ }
+
+ private fun createCloseAnimation(
+ change: TransitionInfo.Change,
+ finishTransaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ ): Animator {
+ finishTransaction.hide(change.leash)
+ return AnimatorSet().apply {
+ playTogether(createBoundsCloseAnimation(change), createAlphaCloseAnimation(change))
+ addListener(onEnd = onAnimFinish)
+ }
+ }
+
+ private fun createBoundsCloseAnimation(change: TransitionInfo.Change): Animator {
+ val startBounds = change.startAbsBounds
+ val endBounds =
+ Rect(startBounds).apply {
+ // Scale the end bounds of the window down with an anchor in the center
+ inset(
+ (startBounds.width().toFloat() * (1 - CLOSE_ANIM_SCALE) / 2).toInt(),
+ (startBounds.height().toFloat() * (1 - CLOSE_ANIM_SCALE) / 2).toInt()
+ )
+ val offsetY =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ CLOSE_ANIM_OFFSET_Y,
+ context.resources.displayMetrics
+ )
+ .toInt()
+ offset(/* dx= */ 0, offsetY)
+ }
+ return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
+ duration = CLOSE_ANIM_DURATION_BOUNDS
+ interpolator = Interpolators.STANDARD_ACCELERATE
+ addUpdateListener { animation ->
+ val animBounds = animation.animatedValue as Rect
+ val animScale = 1 - (1 - CLOSE_ANIM_SCALE) * animation.animatedFraction
+ transactionSupplier
+ .get()
+ .setPosition(change.leash, animBounds.left.toFloat(), animBounds.top.toFloat())
+ .setScale(change.leash, animScale, animScale)
+ .apply()
+ }
+ }
+ }
+
+ private fun createAlphaCloseAnimation(change: TransitionInfo.Change): Animator =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = CLOSE_ANIM_DURATION_ALPHA
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transactionSupplier
+ .get()
+ .setAlpha(change.leash, animation.animatedValue as Float)
+ .apply()
+ }
+ }
+
+ private companion object {
+ const val CLOSE_ANIM_DURATION_BOUNDS = 200L
+ const val CLOSE_ANIM_DURATION_ALPHA = 100L
+ const val CLOSE_ANIM_SCALE = 0.95f
+ const val CLOSE_ANIM_OFFSET_Y = 36.0f
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
new file mode 100644
index 0000000..ec3f8c5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.os.Handler
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.transition.MixedTransitionHandler
+import com.android.wm.shell.transition.Transitions
+
+/** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */
+class DesktopMixedTransitionHandler(
+ private val context: Context,
+ private val transitions: Transitions,
+ private val desktopTaskRepository: DesktopModeTaskRepository,
+ private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler,
+ private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler,
+ private val interactionJankMonitor: InteractionJankMonitor,
+ @ShellMainThread private val handler: Handler,
+) : MixedTransitionHandler, FreeformTaskTransitionStarter {
+
+ /** Delegates starting transition to [FreeformTaskTransitionHandler]. */
+ override fun startWindowingModeTransition(
+ targetWindowingMode: Int,
+ wct: WindowContainerTransaction?,
+ ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct)
+
+ /** Delegates starting minimized mode transition to [FreeformTaskTransitionHandler]. */
+ override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder =
+ freeformTaskTransitionHandler.startMinimizedModeTransition(wct)
+
+ /** Starts close transition and handles or delegates desktop task close animation. */
+ override fun startRemoveTransition(wct: WindowContainerTransaction?) {
+ requireNotNull(wct)
+ transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this)
+ }
+
+ /** Returns null, as it only handles transitions started from Shell. */
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? = null
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ val closeChange = findCloseDesktopTaskChange(info)
+ if (closeChange == null) {
+ ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: Should have closing desktop task", TAG)
+ return false
+ }
+ if (isLastDesktopTask(closeChange)) {
+ // Dispatch close desktop task animation to the default transition handlers.
+ return dispatchCloseLastDesktopTaskAnimation(
+ transition,
+ info,
+ closeChange,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ )
+ }
+ // Animate close desktop task transition with [CloseDesktopTaskTransitionHandler].
+ return closeDesktopTaskTransitionHandler.startAnimation(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ )
+ }
+
+ /**
+ * Dispatch close desktop task animation to the default transition handlers. Allows delegating
+ * it to Launcher to animate in sync with show Home transition.
+ */
+ private fun dispatchCloseLastDesktopTaskAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ change: TransitionInfo.Change,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ // Starting the jank trace if closing the last window in desktop mode.
+ interactionJankMonitor.begin(
+ change.leash,
+ context,
+ handler,
+ CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE,
+ )
+ // Dispatch the last desktop task closing animation.
+ return transitions.dispatchTransition(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ { wct ->
+ // Finish the jank trace when closing the last window in desktop mode.
+ interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE)
+ finishCallback.onTransitionFinished(wct)
+ },
+ /* skip= */ this
+ ) != null
+ }
+
+ private fun isLastDesktopTask(change: TransitionInfo.Change): Boolean =
+ change.taskInfo?.let {
+ desktopTaskRepository.getActiveNonMinimizedTaskCount(it.displayId) == 1
+ } ?: false
+
+ private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? {
+ if (info.type != WindowManager.TRANSIT_CLOSE) return null
+ return info.changes.firstOrNull { change ->
+ change.mode == WindowManager.TRANSIT_CLOSE &&
+ !change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER) &&
+ change.taskInfo?.taskId != INVALID_TASK_ID &&
+ change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+ }
+
+ companion object {
+ private const val TAG = "DesktopMixedTransitionHandler"
+ }
+}
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 02cbe01..b1b7d05 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
@@ -19,6 +19,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.internal.util.FrameworkStatsLog
+import com.android.wm.shell.EventLogTags
import com.android.wm.shell.protolog.ShellProtoLogGroup
/** Event logger for logging desktop mode session events */
@@ -41,6 +42,7 @@
/* exitReason */ 0,
/* session_id */ sessionId
)
+ EventLogTags.writeWmShellEnterDesktopMode(enterReason.reason, sessionId)
}
/**
@@ -61,6 +63,7 @@
/* exitReason */ exitReason.reason,
/* session_id */ sessionId
)
+ EventLogTags.writeWmShellExitDesktopMode(exitReason.reason, sessionId)
}
/**
@@ -135,6 +138,28 @@
/* visible_task_count */
taskUpdate.visibleTaskCount
)
+ EventLogTags.writeWmShellDesktopModeTaskUpdate(
+ /* task_event */
+ taskEvent,
+ /* instance_id */
+ taskUpdate.instanceId,
+ /* uid */
+ taskUpdate.uid,
+ /* task_height */
+ taskUpdate.taskHeight,
+ /* task_width */
+ taskUpdate.taskWidth,
+ /* task_x */
+ taskUpdate.taskX,
+ /* task_y */
+ taskUpdate.taskY,
+ /* session_id */
+ sessionId,
+ taskUpdate.minimizeReason?.reason ?: UNSET_MINIMIZE_REASON,
+ taskUpdate.unminimizeReason?.reason ?: UNSET_UNMINIMIZE_REASON,
+ /* visible_task_count */
+ taskUpdate.visibleTaskCount
+ )
}
companion object {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 0e8c4e7..985224e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -60,6 +60,7 @@
* @property minimizedTasks task ids for active freeform tasks that are currently minimized.
* @property closingTasks task ids for tasks that are going to close, but are currently visible.
* @property freeformTasksInZOrder list of current freeform task ids ordered from top to bottom
+ * @property fullImmersiveTaskId the task id of the desktop task that is in full-immersive mode.
* (top is at index 0).
*/
private data class DesktopTaskData(
@@ -69,13 +70,15 @@
// TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
val closingTasks: ArraySet<Int> = ArraySet(),
val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
+ var fullImmersiveTaskId: Int? = null,
) {
fun deepCopy(): DesktopTaskData = DesktopTaskData(
activeTasks = ArraySet(activeTasks),
visibleTasks = ArraySet(visibleTasks),
minimizedTasks = ArraySet(minimizedTasks),
closingTasks = ArraySet(closingTasks),
- freeformTasksInZOrder = ArrayList(freeformTasksInZOrder)
+ freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
+ fullImmersiveTaskId = fullImmersiveTaskId
)
}
@@ -300,6 +303,23 @@
}
}
+ /** Set whether the given task is the full-immersive task in this display. */
+ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) {
+ val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId)
+ if (immersive) {
+ desktopData.fullImmersiveTaskId = taskId
+ } else {
+ if (desktopData.fullImmersiveTaskId == taskId) {
+ desktopData.fullImmersiveTaskId = null
+ }
+ }
+ }
+
+ /* Whether the task is in full-immersive state. */
+ fun isTaskInFullImmersiveState(taskId: Int): Boolean {
+ return desktopTaskDataSequence().any { taskId == it.fullImmersiveTaskId }
+ }
+
private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) {
visibleTasksListeners.forEach { (listener, executor) ->
executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) }
@@ -330,8 +350,17 @@
/** Minimizes the task for [taskId] and [displayId] */
fun minimizeTask(displayId: Int, taskId: Int) {
- logD("Minimize Task: display=%d, task=%d", displayId, taskId)
- desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
+ if (displayId == INVALID_DISPLAY) {
+ // When a task vanishes it doesn't have a displayId. Find the display of the task and
+ // mark it as minimized.
+ getDisplayIdForTask(taskId)?.let {
+ minimizeTask(it, taskId)
+ } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
+ } else {
+ logD("Minimize Task: display=%d, task=%d", displayId, taskId)
+ desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId)
+ }
+
if (Flags.enableDesktopWindowingPersistence()) {
updatePersistentRepository(displayId)
}
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 125805c..fcd2f8c 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
@@ -44,7 +44,6 @@
import android.view.DragEvent
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
-import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -407,7 +406,7 @@
interactionJankMonitor.begin(taskSurface, context, handler,
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
dragToDesktopTransitionHandler.startDragToDesktopTransition(
- taskInfo.taskId,
+ taskInfo,
dragToDesktopValueAnimator
)
}
@@ -550,7 +549,29 @@
/** Move a task to the front */
fun moveTaskToFront(taskId: Int) {
- shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) }
+ val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
+ if (task == null) moveBackgroundTaskToFront(taskId) else moveTaskToFront(task)
+ }
+
+ /**
+ * Launch a background task in desktop. Note that this should be used when we are already in
+ * desktop. If outside of desktop and want to launch a background task in desktop, use
+ * [moveBackgroundTaskToDesktop] instead.
+ */
+ private fun moveBackgroundTaskToFront(taskId: Int) {
+ logV("moveBackgroundTaskToFront taskId=%s", taskId)
+ val wct = WindowContainerTransaction()
+ // TODO: b/342378842 - Instead of using default display, support multiple displays
+ val taskToMinimize: RunningTaskInfo? =
+ addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId)
+ wct.startTask(
+ taskId,
+ ActivityOptions.makeBasic().apply {
+ launchWindowingMode = WINDOWING_MODE_FREEFORM
+ }.toBundle(),
+ )
+ val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
+ addPendingMinimizeTransition(transition, taskToMinimize)
}
/** Move a task to the front */
@@ -558,7 +579,8 @@
logV("moveTaskToFront taskId=%s", taskInfo.taskId)
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true)
- val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo)
+ val taskToMinimize =
+ addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
addPendingMinimizeTransition(transition, taskToMinimize)
@@ -1254,7 +1276,7 @@
}
// Desktop Mode is showing and we're launching a new Task - we might need to minimize
// a Task.
- val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
if (taskToMinimize != null) {
addPendingMinimizeTransition(transition, taskToMinimize)
return wct
@@ -1280,7 +1302,8 @@
// Desktop Mode is already showing and we're launching a new Task - we might need to
// minimize another Task.
- val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ val taskToMinimize =
+ addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
addPendingMinimizeTransition(transition, taskToMinimize)
}
}
@@ -1313,14 +1336,11 @@
// Remove wallpaper activity when the last active task is removed
removeWallpaperActivity(wct)
}
- taskRepository.addClosingTask(task.displayId, task.taskId)
- // If a CLOSE is triggered on a desktop task, remove the task.
- if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() &&
- taskRepository.isVisibleTask(task.taskId) &&
- transitionType == TRANSIT_CLOSE
- ) {
- wct.removeTask(task.token)
+
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
+ taskRepository.addClosingTask(task.displayId, task.taskId)
}
+
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(
task.displayId,
@@ -1425,12 +1445,12 @@
private fun addAndGetMinimizeChangesIfNeeded(
displayId: Int,
wct: WindowContainerTransaction,
- newTaskInfo: RunningTaskInfo
+ newTaskId: Int
): RunningTaskInfo? {
if (!desktopTasksLimiter.isPresent) return null
return desktopTasksLimiter
.get()
- .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo)
+ .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskId)
}
private fun addPendingMinimizeTransition(
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 d84349b..37ad0c9 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
@@ -24,6 +24,7 @@
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
+import android.window.flags.DesktopModeFlags
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW
import com.android.internal.jank.InteractionJankMonitor
@@ -161,6 +162,8 @@
@VisibleForTesting
inner class LeftoverMinimizedTasksRemover : DesktopModeTaskRepository.ActiveTasksListener {
override fun onActiveTasksChanged(displayId: Int) {
+ // If back navigation is enabled, we shouldn't remove the leftover tasks
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return
val wct = WindowContainerTransaction()
removeLeftoverMinimizedTasks(displayId, wct)
shellTaskOrganizer.applyTransaction(wct)
@@ -208,15 +211,15 @@
fun addAndGetMinimizeTaskChangesIfNeeded(
displayId: Int,
wct: WindowContainerTransaction,
- newFrontTaskInfo: RunningTaskInfo,
+ newFrontTaskId: Int,
): RunningTaskInfo? {
ProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d",
- newFrontTaskInfo.taskId)
+ newFrontTaskId)
val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront(
taskRepository.getActiveNonMinimizedOrderedTasks(displayId),
- newFrontTaskInfo.taskId)
+ newFrontTaskId)
val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack)
if (taskToMinimize != null) {
wct.reorder(taskToMinimize.token, false /* onTop */)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
index 4796c4d..b20c9fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -29,6 +29,7 @@
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
@@ -67,9 +68,29 @@
) {
// TODO: b/332682201 Update repository state
updateWallpaperToken(info)
-
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
handleBackNavigation(info)
+ removeTaskIfNeeded(info)
+ }
+ }
+
+ private fun removeTaskIfNeeded(info: TransitionInfo) {
+ // Since we are no longer removing all the tasks [onTaskVanished], we need to remove them by
+ // checking the transitions.
+ if (!TransitionUtil.isOpeningType(info.type)) return
+ // Remove a task from the repository if the app is launched outside of desktop.
+ for (change in info.changes) {
+ val taskInfo = change.taskInfo
+ if (taskInfo == null || taskInfo.taskId == -1) continue
+
+ if (desktopModeTaskRepository.isActiveTask(taskInfo.taskId)
+ && taskInfo.windowingMode != WINDOWING_MODE_FREEFORM
+ ) {
+ desktopModeTaskRepository.removeFreeformTask(
+ taskInfo.displayId,
+ taskInfo.taskId
+ )
+ }
}
}
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 2bc01b2..8e264b2 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
@@ -109,8 +109,8 @@
* after one of the "end" or "cancel" transitions is merged into this transition.
*/
fun startDragToDesktopTransition(
- taskId: Int,
- dragToDesktopAnimator: MoveToDesktopAnimator,
+ taskInfo: RunningTaskInfo,
+ dragToDesktopAnimator: MoveToDesktopAnimator
) {
if (inProgress) {
ProtoLog.v(
@@ -137,23 +137,26 @@
)
val wct = WindowContainerTransaction()
wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
+ // The home launch done above will result in an attempt to move the task to pip if
+ // applicable, resulting in a broken state. Prevent that here.
+ wct.setDoNotPip(taskInfo.token)
val startTransitionToken =
transitions.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
transitionState =
- if (isSplitTask(taskId)) {
+ if (isSplitTask(taskInfo.taskId)) {
val otherTask =
- getOtherSplitTask(taskId)
+ getOtherSplitTask(taskInfo.taskId)
?: throw IllegalStateException("Expected split task to have a counterpart.")
TransitionState.FromSplit(
- draggedTaskId = taskId,
+ draggedTaskId = taskInfo.taskId,
dragAnimator = dragToDesktopAnimator,
startTransitionToken = startTransitionToken,
otherSplitTask = otherTask
)
} else {
TransitionState.FromFullscreen(
- draggedTaskId = taskId,
+ draggedTaskId = taskInfo.taskId,
dragAnimator = dragToDesktopAnimator,
startTransitionToken = startTransitionToken
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index a1dfb68..68a250d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -45,6 +45,7 @@
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -95,7 +96,25 @@
}
}
.flowOn(backgroundDispatcher)
- .collectLatest { captionState -> showEducation(captionState) }
+ .collectLatest { captionState ->
+ showEducation(captionState)
+ // After showing first tooltip, mark education as viewed
+ appHandleEducationDatastoreRepository.updateEducationViewedTimestampMillis(true)
+ }
+ }
+
+ applicationCoroutineScope.launch {
+ if (isFeatureUsed()) return@launch
+ windowDecorCaptionHandleRepository.captionStateFlow
+ .filter { captionState ->
+ captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded
+ }
+ .take(1)
+ .flowOn(backgroundDispatcher)
+ .collect {
+ // If user expands app handle, mark user has used the feature
+ appHandleEducationDatastoreRepository.updateFeatureUsedTimestampMillis(true)
+ }
}
}
}
@@ -272,6 +291,13 @@
.map { preferences -> preferences.hasEducationViewedTimestampMillis() }
.distinctUntilChanged()
+ /**
+ * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in
+ * datastore proto object.
+ */
+ private suspend fun isFeatureUsed(): Boolean =
+ appHandleEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis()
+
private fun getSize(@DimenRes resourceId: Int): Int {
if (resourceId == Resources.ID_NULL) return 0
return context.resources.getDimensionPixelSize(resourceId)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
index f420c5b..d21b208 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -71,6 +71,37 @@
suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first()
/**
+ * Updates [WindowingEducationProto.educationViewedTimestampMillis_] field in datastore with
+ * current timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateEducationViewedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder()
+ .setEducationViewedTimestampMillis(System.currentTimeMillis())
+ .build()
+ } else {
+ preferences.toBuilder().clearEducationViewedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
+ * Updates [WindowingEducationProto.featureUsedTimestampMillis_] field in datastore with current
+ * timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateFeatureUsedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences.toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build()
+ } else {
+ preferences.toBuilder().clearFeatureUsedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
* Updates [AppHandleEducation.appUsageStats] and
* [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with
* [appUsageStats] and [appUsageStatsLastUpdateTimestamp].
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
index eee5aae..3379ff2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java
@@ -35,6 +35,7 @@
public final ShellTaskOrganizer.TaskListener mTaskListener;
public final Optional<Transitions.TransitionHandler> mTransitionHandler;
public final Optional<Transitions.TransitionObserver> mTransitionObserver;
+ public final Optional<FreeformTaskTransitionStarterInitializer> mTransitionStarterInitializer;
/**
* Creates an instance with the given components.
@@ -42,10 +43,12 @@
public FreeformComponents(
ShellTaskOrganizer.TaskListener taskListener,
Optional<Transitions.TransitionHandler> transitionHandler,
- Optional<Transitions.TransitionObserver> transitionObserver) {
+ Optional<Transitions.TransitionObserver> transitionObserver,
+ Optional<FreeformTaskTransitionStarterInitializer> transitionStarterInitializer) {
mTaskListener = taskListener;
mTransitionHandler = transitionHandler;
mTransitionObserver = transitionObserver;
+ mTransitionStarterInitializer = transitionStarterInitializer;
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 83cc18b..7f7f105 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.util.SparseArray;
import android.view.SurfaceControl;
+import android.window.flags.DesktopModeFlags;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -121,7 +122,16 @@
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
- repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+ // TODO: b/370038902 - Handle Activity#finishAndRemoveTask.
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()
+ || repository.isClosingTask(taskInfo.taskId)) {
+ // A task that's vanishing should be removed:
+ // - If it's closed by the X button which means it's marked as a closing task.
+ repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+ } else {
+ repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false);
+ repository.minimizeTask(taskInfo.displayId, taskInfo.taskId);
+ }
});
}
mWindowDecorationViewModel.onTaskVanished(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 517e209..6aaf001 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -19,16 +19,12 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE;
-
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
-import android.content.Context;
import android.graphics.Rect;
-import android.os.Handler;
import android.os.IBinder;
import android.util.ArrayMap;
import android.view.SurfaceControl;
@@ -40,14 +36,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.shared.annotations.ShellMainThread;
-import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import java.util.ArrayList;
import java.util.List;
@@ -59,48 +50,24 @@
public class FreeformTaskTransitionHandler
implements Transitions.TransitionHandler, FreeformTaskTransitionStarter {
private static final int CLOSE_ANIM_DURATION = 400;
- private final Context mContext;
private final Transitions mTransitions;
- private final WindowDecorViewModel mWindowDecorViewModel;
- private final DesktopModeTaskRepository mDesktopModeTaskRepository;
private final DisplayController mDisplayController;
- private final InteractionJankMonitor mInteractionJankMonitor;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
- @ShellMainThread
- private final Handler mHandler;
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
public FreeformTaskTransitionHandler(
- ShellInit shellInit,
Transitions transitions,
- Context context,
- WindowDecorViewModel windowDecorViewModel,
DisplayController displayController,
ShellExecutor mainExecutor,
- ShellExecutor animExecutor,
- DesktopModeTaskRepository desktopModeTaskRepository,
- InteractionJankMonitor interactionJankMonitor,
- @ShellMainThread Handler handler) {
+ ShellExecutor animExecutor) {
mTransitions = transitions;
- mContext = context;
- mWindowDecorViewModel = windowDecorViewModel;
- mDesktopModeTaskRepository = desktopModeTaskRepository;
mDisplayController = displayController;
- mInteractionJankMonitor = interactionJankMonitor;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
- mHandler = handler;
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- shellInit.addInitCallback(this::onInit, this);
- }
- }
-
- private void onInit() {
- mWindowDecorViewModel.setFreeformTaskTransitionStarter(this);
}
@Override
@@ -269,20 +236,12 @@
startBounds.top + (animation.getAnimatedFraction() * screenHeight));
t.apply();
});
- if (mDesktopModeTaskRepository.getActiveNonMinimizedTaskCount(
- change.getTaskInfo().displayId) == 1) {
- // Starting the jank trace if closing the last window in desktop mode.
- mInteractionJankMonitor.begin(
- sc, mContext, mHandler, CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE);
- }
animator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
animations.remove(animator);
onAnimFinish.run();
- mInteractionJankMonitor.end(
- CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE);
}
});
animations.add(animator);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
new file mode 100644
index 0000000..98bdf05
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.freeform
+
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.windowdecor.WindowDecorViewModel
+
+/**
+ * Sets up [FreeformTaskTransitionStarter] for [WindowDecorViewModel] when shell finishes
+ * initializing.
+ *
+ * Used to extract the setup logic from the starter implementation.
+ */
+class FreeformTaskTransitionStarterInitializer(
+ shellInit: ShellInit,
+ private val windowDecorViewModel: WindowDecorViewModel,
+ private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter
+) {
+ init {
+ shellInit.addInitCallback(::onShellInit, this)
+ }
+
+ /** Sets up [WindowDecorViewModel] transition starter with [FreeformTaskTransitionStarter] */
+ private fun onShellInit() {
+ windowDecorViewModel.setFreeformTaskTransitionStarter(freeformTaskTransitionStarter)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 1b9bf2a..dc0bc78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -304,54 +304,28 @@
if (pipChange == null) {
return false;
}
- WindowContainerToken pipTaskToken = pipChange.getContainer();
SurfaceControl pipLeash = pipChange.getLeash();
+ Preconditions.checkNotNull(pipLeash, "Leash is null for swipe-up transition.");
- if (pipTaskToken == null || pipLeash == null) {
- return false;
- }
-
- SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay();
- PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
-
- Rect appBounds = mPipTransitionState.getSwipePipToHomeAppBounds();
- Rect destinationBounds = pipChange.getEndAbsBounds();
-
- float aspectRatio = pipChange.getTaskInfo().pictureInPictureParams.getAspectRatioFloat();
-
- // We fake the source rect hint when the one prvided by the app is invalid for
- // the animation with an app icon overlay.
- Rect animationSrcRectHint = overlayLeash == null ? params.getSourceRectHint()
- : PipUtils.getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio);
-
- WindowContainerTransaction finishWct = new WindowContainerTransaction();
- SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-
- final float scale = (float) destinationBounds.width() / animationSrcRectHint.width();
- startTransaction.setWindowCrop(pipLeash, animationSrcRectHint);
- startTransaction.setPosition(pipLeash,
- destinationBounds.left - animationSrcRectHint.left * scale,
- destinationBounds.top - animationSrcRectHint.top * scale);
- startTransaction.setScale(pipLeash, scale, scale);
-
- if (overlayLeash != null) {
+ final Rect destinationBounds = pipChange.getEndAbsBounds();
+ final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay();
+ if (swipePipToHomeOverlay != null) {
final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize(
mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds);
-
- // Overlay needs to be adjusted once a new draw comes in resetting surface transform.
- tx.setScale(overlayLeash, 1f, 1f);
- tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f,
- (destinationBounds.height() - overlaySize) / 2f);
+ // It is possible we reparent the PIP activity to a new PIP task (in multi-activity
+ // apps), so we should also reparent the overlay to the final PIP task.
+ startTransaction.reparent(swipePipToHomeOverlay, pipLeash)
+ .setLayer(swipePipToHomeOverlay, Integer.MAX_VALUE)
+ .setScale(swipePipToHomeOverlay, 1f, 1f)
+ .setPosition(swipePipToHomeOverlay,
+ (destinationBounds.width() - overlaySize) / 2f,
+ (destinationBounds.height() - overlaySize) / 2f);
}
+
+ startTransaction.merge(finishTransaction);
startTransaction.apply();
-
- tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
- this::onClientDrawAtTransitionEnd);
- finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
-
- // Note that finishWct should be free of any actual WM state changes; we are using
- // it for syncing with the client draw after delayed configuration changes are dispatched.
- finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
+ finishCallback.onTransitionFinished(null /* finishWct */);
+ onClientDrawAtTransitionEnd();
return true;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 03ff1aa..c9c0873 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -32,6 +32,7 @@
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
+import android.graphics.Point;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Slog;
@@ -46,6 +47,7 @@
import androidx.annotation.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
+import com.android.window.flags.Flags;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
@@ -421,6 +423,16 @@
if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
mostRecentFreeformTaskIndex = recentTasks.size();
}
+ // If task has their app bounds set to null which happens after reboot, set the
+ // app bounds to persisted lastFullscreenBounds. Also set the position in parent
+ // to the top left of the bounds.
+ if (Flags.enableDesktopWindowingPersistence()
+ && taskInfo.configuration.windowConfiguration.getAppBounds() == null) {
+ taskInfo.configuration.windowConfiguration.setAppBounds(
+ taskInfo.lastNonFullscreenBounds);
+ taskInfo.positionInParent = new Point(taskInfo.lastNonFullscreenBounds.left,
+ taskInfo.lastNonFullscreenBounds.top);
+ }
freeformTasks.add(taskInfo);
if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
minimizedFreeformTasks.add(taskInfo.taskId);
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 8077aee..f7ed1dd 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
@@ -52,7 +52,6 @@
import android.util.IntArray;
import android.util.Pair;
import android.util.Slog;
-import android.view.Display;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.PictureInPictureSurfaceTransaction;
@@ -910,6 +909,14 @@
"task #" + taskInfo.taskId + " is always_on_top");
return;
}
+ if (TransitionUtil.isClosingType(change.getMode())
+ && taskInfo != null && taskInfo.lastParentTaskIdBeforePip > 0) {
+ // Pinned task is closing as a side effect of the removal of its original Task,
+ // such transition should be handled by PiP. So cancel the merge here.
+ cancel(false /* toHome */, false /* withScreenshots */,
+ "task #" + taskInfo.taskId + " is removed with its original parent");
+ return;
+ }
final boolean isRootTask = taskInfo != null
&& TransitionInfo.isIndependent(change, info);
final boolean isRecentsTask = mRecentsTask != null
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 6e084d6..2747249 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -146,6 +146,11 @@
Slog.w(TAG, "Failed to relayout snapshot starting window");
return null;
}
+ if (!surfaceControl.isValid()) {
+ snapshotSurface.clearWindowSynced();
+ Slog.w(TAG, "Unable to draw snapshot, no valid surface");
+ return null;
+ }
SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot,
info.taskBounds, topWindowInsetsState, true /* releaseAfterDraw */);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
index 2f5059f..399e39a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -17,13 +17,13 @@
package com.android.wm.shell.transition;
import static android.view.Display.INVALID_DISPLAY;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
import static com.android.wm.shell.transition.Transitions.TransitionObserver;
import android.annotation.NonNull;
-import android.app.ActivityManager.RunningTaskInfo;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
@@ -62,10 +62,9 @@
final List<TransitionInfo.Change> changes = info.getChanges();
for (int i = changes.size() - 1; i >= 0; i--) {
final TransitionInfo.Change change = changes.get(i);
- final RunningTaskInfo task = change.getTaskInfo();
- if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) {
- if (mFocusedDisplayId != task.displayId) {
- mFocusedDisplayId = task.displayId;
+ if (change.hasFlags(FLAG_IS_DISPLAY) && change.hasFlags(FLAG_MOVED_TO_TOP)) {
+ if (mFocusedDisplayId != change.getEndDisplayId()) {
+ mFocusedDisplayId = change.getEndDisplayId();
notifyFocusedDisplayChanged();
}
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d280dcd..d5e92e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -1036,9 +1036,14 @@
* Gives every handler (in order) a chance to animate until one consumes the transition.
* @return the handler which consumed the transition.
*/
- TransitionHandler dispatchTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT,
- @NonNull TransitionFinishCallback finishCB, @Nullable TransitionHandler skip) {
+ public TransitionHandler dispatchTransition(
+ @NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull TransitionFinishCallback finishCB,
+ @Nullable TransitionHandler skip
+ ) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
if (mHandlers.get(i) == skip) continue;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 05065be..839973f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -194,6 +194,8 @@
ActivityManager.RunningTaskInfo taskInfo,
boolean applyStartTransactionOnDraw,
boolean setTaskCropAndPosition,
+ boolean isStatusBarVisible,
+ boolean isKeyguardVisibleAndOccluded,
InsetsState displayInsetsState) {
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
@@ -204,6 +206,8 @@
: R.dimen.freeform_decor_shadow_unfocused_thickness;
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition;
+ relayoutParams.mIsCaptionVisible = taskInfo.isFreeform()
+ || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -240,7 +244,8 @@
final WindowContainerTransaction wct = new WindowContainerTransaction();
updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
- setTaskCropAndPosition, mDisplayController.getInsetsState(taskInfo.displayId));
+ setTaskCropAndPosition, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
+ mDisplayController.getInsetsState(taskInfo.displayId));
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index c34a0bc..3330f96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -22,9 +22,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_PINNED;
-import static android.content.Intent.ACTION_MAIN;
-import static android.content.Intent.CATEGORY_APP_BROWSER;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
@@ -58,7 +55,6 @@
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
-import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -107,6 +103,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
@@ -158,6 +155,7 @@
private final ActivityTaskManager mActivityTaskManager;
private final ShellCommandHandler mShellCommandHandler;
private final ShellTaskOrganizer mTaskOrganizer;
+ private final DesktopModeTaskRepository mDesktopRepository;
private final ShellController mShellController;
private final Context mContext;
private final @ShellMainThread Handler mMainHandler;
@@ -229,6 +227,7 @@
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
ShellTaskOrganizer taskOrganizer,
+ DesktopModeTaskRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -254,6 +253,7 @@
shellCommandHandler,
windowManager,
taskOrganizer,
+ desktopRepository,
displayController,
shellController,
displayInsetsController,
@@ -288,6 +288,7 @@
ShellCommandHandler shellCommandHandler,
IWindowManager windowManager,
ShellTaskOrganizer taskOrganizer,
+ DesktopModeTaskRepository desktopRepository,
DisplayController displayController,
ShellController shellController,
DisplayInsetsController displayInsetsController,
@@ -316,6 +317,7 @@
mBgExecutor = bgExecutor;
mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
mTaskOrganizer = taskOrganizer;
+ mDesktopRepository = desktopRepository;
mShellController = shellController;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
@@ -560,20 +562,17 @@
decoration.closeMaximizeMenu();
}
- private void onOpenInBrowser(int taskId, @NonNull Uri uri) {
+ private void onOpenInBrowser(int taskId, @NonNull Intent intent) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
if (decoration == null) {
return;
}
- openInBrowser(uri, decoration.getUser());
+ openInBrowser(intent, decoration.getUser());
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
}
- private void openInBrowser(Uri uri, @NonNull UserHandle userHandle) {
- final Intent intent = Intent.makeMainSelectorActivity(ACTION_MAIN, CATEGORY_APP_BROWSER)
- .setData(uri)
- .addFlags(FLAG_ACTIVITY_NEW_TASK);
+ private void openInBrowser(@NonNull Intent intent, @NonNull UserHandle userHandle) {
mContext.startActivityAsUser(intent, userHandle);
}
@@ -1421,6 +1420,7 @@
mContext.createContextAsUser(UserHandle.of(taskInfo.userId), 0 /* flags */),
mDisplayController,
mSplitScreenController,
+ mDesktopRepository,
mTaskOrganizer,
taskInfo,
taskSurface,
@@ -1472,8 +1472,8 @@
onToSplitScreen(taskInfo.taskId);
return Unit.INSTANCE;
});
- windowDecoration.setOpenInBrowserClickListener((uri) -> {
- onOpenInBrowser(taskInfo.taskId, uri);
+ windowDecoration.setOpenInBrowserClickListener((intent) -> {
+ onOpenInBrowser(taskInfo.taskId, intent);
});
windowDecoration.setOnNewWindowClickListener(() -> {
onNewWindow(taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 99457d8..5daa3ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -44,12 +44,14 @@
import android.app.assist.AssistContent;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -62,10 +64,12 @@
import android.util.Size;
import android.util.Slog;
import android.view.Choreographer;
+import android.view.InsetsState;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.window.TaskSnapshot;
@@ -89,6 +93,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -166,7 +171,7 @@
private CapturedLink mCapturedLink;
private Uri mGenericLink;
private Uri mWebUri;
- private Consumer<Uri> mOpenInBrowserClickListener;
+ private Consumer<Intent> mOpenInBrowserClickListener;
private ExclusionRegionListener mExclusionRegionListener;
@@ -188,12 +193,14 @@
private final Runnable mCapturedLinkExpiredRunnable = this::onCapturedLinkExpired;
private final MultiInstanceHelper mMultiInstanceHelper;
private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
+ private final DesktopModeTaskRepository mDesktopRepository;
DesktopModeWindowDecoration(
Context context,
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
+ DesktopModeTaskRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -207,8 +214,8 @@
AssistContentRequester assistContentRequester,
MultiInstanceHelper multiInstanceHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository) {
- this (context, userContext, displayController, splitScreenController, taskOrganizer,
- taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
+ this (context, userContext, displayController, splitScreenController, desktopRepository,
+ taskOrganizer, taskInfo, taskSurface, handler, bgExecutor, choreographer, syncQueue,
appHeaderViewHolderFactory, rootTaskDisplayAreaOrganizer, genericLinksParser,
assistContentRequester,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
@@ -225,6 +232,7 @@
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
+ DesktopModeTaskRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -264,6 +272,7 @@
mMultiInstanceHelper = multiInstanceHelper;
mWindowManagerWrapper = windowManagerWrapper;
mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
+ mDesktopRepository = desktopRepository;
}
/**
@@ -335,7 +344,7 @@
mDragPositioningCallback = dragPositioningCallback;
}
- void setOpenInBrowserClickListener(Consumer<Uri> listener) {
+ void setOpenInBrowserClickListener(Consumer<Intent> listener) {
mOpenInBrowserClickListener = listener;
}
@@ -439,8 +448,11 @@
mHandleMenu.relayout(startT, mResult.mCaptionX);
}
+ final boolean inFullImmersive = mDesktopRepository
+ .isTaskInFullImmersiveState(taskInfo.taskId);
updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
- shouldSetTaskPositionAndCrop);
+ shouldSetTaskPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded,
+ inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId));
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
@@ -480,11 +492,17 @@
if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
notifyCaptionStateChanged();
}
- mWindowDecorViewHolder.bindData(mTaskInfo,
- position,
- mResult.mCaptionWidth,
- mResult.mCaptionHeight,
- isCaptionVisible());
+
+ if (isAppHandle(mWindowDecorViewHolder)) {
+ mWindowDecorViewHolder.bindData(new AppHandleViewHolder.HandleData(
+ mTaskInfo, position, mResult.mCaptionWidth, mResult.mCaptionHeight,
+ isCaptionVisible()
+ ));
+ } else {
+ mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData(
+ mTaskInfo, TaskInfoKt.getRequestingImmersive(mTaskInfo), inFullImmersive
+ ));
+ }
Trace.endSection();
if (!mTaskInfo.isFocused) {
@@ -518,21 +536,28 @@
}
@Nullable
- private Uri getBrowserLink() {
+ private Intent getBrowserLink() {
// Do not show browser link in browser applications
final ComponentName baseActivity = mTaskInfo.baseActivity;
if (baseActivity != null && AppToWebUtils.isBrowserApp(mContext,
baseActivity.getPackageName(), mUserContext.getUserId())) {
return null;
}
+
+ final Uri browserLink;
// If the captured link is available and has not expired, return the captured link.
// Otherwise, return the generic link which is set to null if a generic link is unavailable.
if (mCapturedLink != null && !mCapturedLink.mExpired) {
- return mCapturedLink.mUri;
+ browserLink = mCapturedLink.mUri;
} else if (mWebUri != null) {
- return mWebUri;
+ browserLink = mWebUri;
+ } else {
+ browserLink = mGenericLink;
}
- return mGenericLink;
+
+ if (browserLink == null) return null;
+ return AppToWebUtils.getBrowserIntent(browserLink, mContext.getPackageManager());
+
}
UserHandle getUser() {
@@ -738,7 +763,11 @@
Context context,
ActivityManager.RunningTaskInfo taskInfo,
boolean applyStartTransactionOnDraw,
- boolean shouldSetTaskPositionAndCrop) {
+ boolean shouldSetTaskPositionAndCrop,
+ boolean isStatusBarVisible,
+ boolean isKeyguardVisibleAndOccluded,
+ boolean inFullImmersiveMode,
+ @NonNull InsetsState displayInsetsState) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
final boolean isAppHeader =
captionLayoutId == R.layout.desktop_mode_app_header;
@@ -749,6 +778,28 @@
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
+ final boolean showCaption;
+ if (Flags.enableFullyImmersiveInDesktop()) {
+ if (inFullImmersiveMode) {
+ showCaption = isStatusBarVisible && !isKeyguardVisibleAndOccluded;
+ } else {
+ showCaption = taskInfo.isFreeform()
+ || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
+ }
+ } else {
+ // Caption should always be visible in freeform mode. When not in freeform,
+ // align with the status bar except when showing over keyguard (where it should not
+ // shown).
+ // TODO(b/356405803): Investigate how it's possible for the status bar visibility to
+ // be false while a freeform window is open if the status bar is always
+ // forcibly-shown. It may be that the InsetsState (from which |mIsStatusBarVisible|
+ // is set) still contains an invisible insets source in immersive cases even if the
+ // status bar is shown?
+ showCaption = taskInfo.isFreeform()
+ || (isStatusBarVisible && !isKeyguardVisibleAndOccluded);
+ }
+ relayoutParams.mIsCaptionVisible = showCaption;
+
if (isAppHeader) {
if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
// If the app is requesting to customize the caption bar, allow input to fall
@@ -767,6 +818,13 @@
// including non-immersive apps that just don't handle caption insets properly.
relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
}
+ if (Flags.enableFullyImmersiveInDesktop() && inFullImmersiveMode) {
+ final Insets systemBarInsets = displayInsetsState.calculateInsets(
+ taskInfo.getConfiguration().windowConfiguration.getBounds(),
+ WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(),
+ false /* ignoreVisibility */);
+ relayoutParams.mCaptionTopPadding = systemBarInsets.top;
+ }
// Report occluding elements as bounding rects to the insets system so that apps can
// draw in the empty space in the center:
// First, the "app chip" section of the caption bar (+ some extra margins).
@@ -1049,7 +1107,7 @@
}
/**
- * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs.
+ * Determine the highest y coordinate of a freeform task. Used for restricting drag inputs.fmdra
*/
private int determineMaxY(int requiredEmptySpace, Rect stableBounds) {
return stableBounds.bottom - requiredEmptySpace;
@@ -1172,8 +1230,8 @@
/* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener,
/* onNewWindowClickListener= */ mOnNewWindowClickListener,
/* onManageWindowsClickListener= */ mOnManageWindowsClickListener,
- /* openInBrowserClickListener= */ (uri) -> {
- mOpenInBrowserClickListener.accept(uri);
+ /* openInBrowserClickListener= */ (intent) -> {
+ mOpenInBrowserClickListener.accept(intent);
onCapturedLinkExpired();
return Unit.INSTANCE;
},
@@ -1532,6 +1590,7 @@
@NonNull Context userContext,
DisplayController displayController,
SplitScreenController splitScreenController,
+ DesktopModeTaskRepository desktopRepository,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -1550,6 +1609,7 @@
userContext,
displayController,
splitScreenController,
+ desktopRepository,
taskOrganizer,
taskInfo,
taskSurface,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 9a5b4f5..98fef47 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -20,13 +20,13 @@
import android.annotation.SuppressLint
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
+import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
-import android.net.Uri
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_OUTSIDE
@@ -70,7 +70,7 @@
private val shouldShowWindowingPill: Boolean,
private val shouldShowNewWindowButton: Boolean,
private val shouldShowManageWindowsButton: Boolean,
- private val openInBrowserLink: Uri?,
+ private val openInBrowserIntent: Intent?,
private val captionWidth: Int,
private val captionHeight: Int,
captionX: Int
@@ -107,7 +107,7 @@
private val globalMenuPosition: Point = Point()
private val shouldShowBrowserPill: Boolean
- get() = openInBrowserLink != null
+ get() = openInBrowserIntent != null
init {
updateHandleMenuPillPositions(captionX)
@@ -119,7 +119,7 @@
onToSplitScreenClickListener: () -> Unit,
onNewWindowClickListener: () -> Unit,
onManageWindowsClickListener: () -> Unit,
- openInBrowserClickListener: (Uri) -> Unit,
+ openInBrowserClickListener: (Intent) -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit,
) {
@@ -152,7 +152,7 @@
onToSplitScreenClickListener: () -> Unit,
onNewWindowClickListener: () -> Unit,
onManageWindowsClickListener: () -> Unit,
- openInBrowserClickListener: (Uri) -> Unit,
+ openInBrowserClickListener: (Intent) -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit
) {
@@ -172,7 +172,7 @@
this.onNewWindowClickListener = onNewWindowClickListener
this.onManageWindowsClickListener = onManageWindowsClickListener
this.onOpenInBrowserClickListener = {
- openInBrowserClickListener.invoke(openInBrowserLink!!)
+ openInBrowserClickListener.invoke(openInBrowserIntent!!)
}
this.onCloseMenuClickListener = onCloseMenuClickListener
this.onOutsideTouchListener = onOutsideTouchListener
@@ -661,7 +661,7 @@
shouldShowWindowingPill: Boolean,
shouldShowNewWindowButton: Boolean,
shouldShowManageWindowsButton: Boolean,
- openInBrowserLink: Uri?,
+ openInBrowserIntent: Intent?,
captionWidth: Int,
captionHeight: Int,
captionX: Int
@@ -680,7 +680,7 @@
shouldShowWindowingPill: Boolean,
shouldShowNewWindowButton: Boolean,
shouldShowManageWindowsButton: Boolean,
- openInBrowserLink: Uri?,
+ openInBrowserIntent: Intent?,
captionWidth: Int,
captionHeight: Int,
captionX: Int
@@ -695,7 +695,7 @@
shouldShowWindowingPill,
shouldShowNewWindowButton,
shouldShowManageWindowsButton,
- openInBrowserLink,
+ openInBrowserIntent,
captionWidth,
captionHeight,
captionX
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index c1a55b4..000beba1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -144,8 +144,8 @@
TaskDragResizer mTaskDragResizer;
boolean mIsCaptionVisible;
- private boolean mIsStatusBarVisible;
- private boolean mIsKeyguardVisibleAndOccluded;
+ boolean mIsStatusBarVisible;
+ boolean mIsKeyguardVisibleAndOccluded;
/** The most recent set of insets applied to this window decoration. */
private WindowDecorationInsets mWindowDecorationInsets;
@@ -241,7 +241,7 @@
}
rootView = null; // Clear it just in case we use it accidentally
- updateCaptionVisibility(outResult.mRootView);
+ updateCaptionVisibility(outResult.mRootView, params);
final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
outResult.mWidth = taskBounds.width();
@@ -527,17 +527,10 @@
}
/**
- * Checks if task has entered/exited immersive mode and requires a change in caption visibility.
+ * Update caption visibility state and views.
*/
- private void updateCaptionVisibility(View rootView) {
- // Caption should always be visible in freeform mode. When not in freeform, align with the
- // status bar except when showing over keyguard (where it should not shown).
- // TODO(b/356405803): Investigate how it's possible for the status bar visibility to be
- // false while a freeform window is open if the status bar is always forcibly-shown. It
- // may be that the InsetsState (from which |mIsStatusBarVisible| is set) still contains
- // an invisible insets source in immersive cases even if the status bar is shown?
- mIsCaptionVisible = mTaskInfo.isFreeform()
- || (mIsStatusBarVisible && !mIsKeyguardVisibleAndOccluded);
+ private void updateCaptionVisibility(View rootView, @NonNull RelayoutParams params) {
+ mIsCaptionVisible = params.mIsCaptionVisible;
setCaptionVisibility(rootView, mIsCaptionVisible);
}
@@ -737,6 +730,7 @@
int mCornerRadius;
int mCaptionTopPadding;
+ boolean mIsCaptionVisible;
Configuration mWindowDecorConfig;
@@ -755,6 +749,7 @@
mCornerRadius = 0;
mCaptionTopPadding = 0;
+ mIsCaptionVisible = false;
mApplyStartTransactionOnDraw = false;
mSetTaskPositionAndCrop = false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
index 98413ee..a9a16bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
@@ -30,9 +30,13 @@
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
+import android.window.DisplayAreaInfo
+import android.window.WindowContainerTransaction
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.wm.shell.R
+import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener
+import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.shared.animation.PhysicsAnimator
import com.android.wm.shell.windowdecor.WindowManagerWrapper
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
@@ -44,7 +48,8 @@
class DesktopWindowingEducationTooltipController(
private val context: Context,
private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory,
-) {
+ private val displayController: DisplayController,
+) : OnDisplayChangingListener {
// TODO: b/369384567 - Set tooltip color scheme to match LT/DT of app theme
private var tooltipView: View? = null
private var animator: PhysicsAnimator<View>? = null
@@ -53,6 +58,20 @@
}
private var popupWindow: AdditionalSystemViewContainer? = null
+ override fun onDisplayChange(
+ displayId: Int,
+ fromRotation: Int,
+ toRotation: Int,
+ newDisplayAreaInfo: DisplayAreaInfo?,
+ t: WindowContainerTransaction?
+ ) {
+ // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and
+ // [toRotation] can be one of the [@Surface.Rotation] values.
+ if ((fromRotation % 2 == toRotation % 2)) return
+ hideEducationTooltip()
+ // TODO: b/370820018 - Update tooltip position on orientation change instead of dismissing
+ }
+
/**
* Shows education tooltip.
*
@@ -64,6 +83,7 @@
tooltipView = createEducationTooltipView(tooltipViewConfig, taskId)
animator = createAnimator()
animateShowTooltipTransition()
+ displayController.addDisplayChangingController(this)
}
/** Hide the current education view if visible */
@@ -145,6 +165,7 @@
animator = null
popupWindow?.releaseView()
popupWindow = null
+ displayController.removeDisplayChangingController(this)
}
private fun createTooltipPopupWindow(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 8c102eb..b5700ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -42,6 +42,7 @@
import com.android.wm.shell.shared.animation.Interpolators
import com.android.wm.shell.windowdecor.WindowManagerWrapper
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data
/**
* A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
@@ -53,11 +54,20 @@
onCaptionButtonClickListener: OnClickListener,
private val windowManagerWrapper: WindowManagerWrapper,
private val handler: Handler
-) : WindowDecorationViewHolder(rootView) {
+) : WindowDecorationViewHolder<AppHandleViewHolder.HandleData>(rootView) {
companion object {
private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
}
+
+ data class HandleData(
+ val taskInfo: RunningTaskInfo,
+ val position: Point,
+ val width: Int,
+ val height: Int,
+ val isCaptionVisible: Boolean
+ ) : Data()
+
private lateinit var taskInfo: RunningTaskInfo
private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
@@ -89,7 +99,11 @@
}
}
- override fun bindData(
+ override fun bindData(data: HandleData) {
+ bindData(data.taskInfo, data.position, data.width, data.height, data.isCaptionVisible)
+ }
+
+ private fun bindData(
taskInfo: RunningTaskInfo,
position: Point,
width: Int,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 306103c..52bf400 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -16,12 +16,12 @@
package com.android.wm.shell.windowdecor.viewholder
import android.annotation.ColorInt
+import android.annotation.DrawableRes
import android.app.ActivityManager.RunningTaskInfo
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
-import android.graphics.Point
import android.graphics.Rect
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
@@ -60,7 +60,6 @@
import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
-import com.android.wm.shell.windowdecor.extension.requestingImmersive
/**
* A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
@@ -76,7 +75,13 @@
appName: CharSequence,
appIconBitmap: Bitmap,
onMaximizeHoverAnimationFinishedListener: () -> Unit
-) : WindowDecorationViewHolder(rootView) {
+) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) {
+
+ data class HeaderData(
+ val taskInfo: RunningTaskInfo,
+ val isRequestingImmersive: Boolean,
+ val inFullImmersiveState: Boolean,
+ ) : Data()
private val decorThemeUtil = DecorThemeUtil(context)
private val lightColors = dynamicLightColorScheme(context)
@@ -153,15 +158,17 @@
onMaximizeHoverAnimationFinishedListener
}
- override fun bindData(
+ override fun bindData(data: HeaderData) {
+ bindData(data.taskInfo, data.isRequestingImmersive, data.inFullImmersiveState)
+ }
+
+ private fun bindData(
taskInfo: RunningTaskInfo,
- position: Point,
- width: Int,
- height: Int,
- isCaptionVisible: Boolean
+ isRequestingImmersive: Boolean,
+ inFullImmersiveState: Boolean,
) {
if (DesktopModeFlags.ENABLE_THEMED_APP_HEADERS.isTrue()) {
- bindDataWithThemedHeaders(taskInfo)
+ bindDataWithThemedHeaders(taskInfo, isRequestingImmersive, inFullImmersiveState)
} else {
bindDataLegacy(taskInfo)
}
@@ -200,7 +207,11 @@
minimizeWindowButton.isGone = !enableMinimizeButton()
}
- private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) {
+ private fun bindDataWithThemedHeaders(
+ taskInfo: RunningTaskInfo,
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ) {
val header = fillHeaderInfo(taskInfo)
val headerStyle = getHeaderStyle(header)
@@ -254,13 +265,7 @@
drawableInsets = maximizeDrawableInsets
)
)
- setIcon(
- if (taskInfo.requestingImmersive && Flags.enableFullyImmersiveInDesktop()) {
- R.drawable.decor_desktop_mode_immersive_button_dark
- } else {
- R.drawable.decor_desktop_mode_maximize_button_dark
- }
- )
+ setIcon(getMaximizeButtonIcon(requestingImmersive, inFullImmersiveState))
}
// Close button.
closeWindowButton.apply {
@@ -331,6 +336,32 @@
}
}
+ @DrawableRes
+ private fun getMaximizeButtonIcon(
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ): Int = when {
+ shouldShowEnterFullImmersiveIcon(requestingImmersive, inFullImmersiveState) -> {
+ R.drawable.decor_desktop_mode_immersive_button_dark
+ }
+ shouldShowExitFullImmersiveIcon(requestingImmersive, inFullImmersiveState) -> {
+ R.drawable.decor_desktop_mode_immersive_exit_button_dark
+ }
+ else -> R.drawable.decor_desktop_mode_maximize_button_dark
+ }
+
+ private fun shouldShowEnterFullImmersiveIcon(
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ): Boolean = Flags.enableFullyImmersiveInDesktop()
+ && requestingImmersive && !inFullImmersiveState
+
+ private fun shouldShowExitFullImmersiveIcon(
+ requestingImmersive: Boolean,
+ inFullImmersiveState: Boolean
+ ): Boolean = Flags.enableFullyImmersiveInDesktop()
+ && requestingImmersive && inFullImmersiveState
+
private fun getHeaderStyle(header: Header): HeaderStyle {
return HeaderStyle(
background = getHeaderBackground(header),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 5ea55b3..1fe743d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -17,31 +17,28 @@
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
-import android.graphics.Point
import android.view.View
+import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data
/**
* Encapsulates the root [View] of a window decoration and its children to facilitate looking up
* children (via findViewById) and updating to the latest data from [RunningTaskInfo].
*/
-abstract class WindowDecorationViewHolder(rootView: View) {
+abstract class WindowDecorationViewHolder<T : Data>(rootView: View) {
val context: Context = rootView.context
/**
* A signal to the view holder that new data is available and that the views should be updated to
* reflect it.
*/
- abstract fun bindData(
- taskInfo: RunningTaskInfo,
- position: Point,
- width: Int,
- height: Int,
- isCaptionVisible: Boolean
- )
+ abstract fun bindData(data: T)
/** Callback when the handle menu is opened. */
abstract fun onHandleMenuOpened()
/** Callback when the handle menu is closed. */
abstract fun onHandleMenuClosed()
+
+ /** Data clas that contains the information needed to update the view holder. */
+ abstract class Data
}
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
new file mode 100644
index 0000000..9b4cc17
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.ShellExecutor
+import java.util.function.Supplier
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.kotlin.mock
+
+/**
+ * Test class for [CloseDesktopTaskTransitionHandler]
+ *
+ * Usage: atest WMShellUnitTests:CloseDesktopTaskTransitionHandlerTest
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() {
+
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock lateinit var closingTaskLeash: SurfaceControl
+
+ private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
+
+ private lateinit var handler: CloseDesktopTaskTransitionHandler
+
+ @Before
+ fun setUp() {
+ handler =
+ CloseDesktopTaskTransitionHandler(
+ context,
+ testExecutor,
+ testExecutor,
+ transactionSupplier
+ )
+ }
+
+ @Test
+ fun handleRequest_returnsNull() {
+ assertNull(handler.handleRequest(mock(), mock()))
+ }
+
+ @Test
+ fun startAnimation_openTransition_returnsFalse() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info =
+ createTransitionInfo(
+ type = WindowManager.TRANSIT_OPEN,
+ task = createTask(WINDOWING_MODE_FREEFORM)
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not animate open transition", animates)
+ }
+
+ @Test
+ fun startAnimation_closeTransitionFullscreenTask_returnsFalse() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not animate fullscreen task close transition", animates)
+ }
+
+ @Test
+ fun startAnimation_closeTransitionOpeningFreeformTask_returnsFalse() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info =
+ createTransitionInfo(
+ changeMode = WindowManager.TRANSIT_OPEN,
+ task = createTask(WINDOWING_MODE_FREEFORM)
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not animate opening freeform task close transition", animates)
+ }
+
+ @Test
+ fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() {
+ val animates =
+ handler.startAnimation(
+ transition = mock(),
+ info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertTrue("Should animate closing freeform task close transition", animates)
+ }
+
+ private fun createTransitionInfo(
+ type: Int = WindowManager.TRANSIT_CLOSE,
+ changeMode: Int = WindowManager.TRANSIT_CLOSE,
+ task: RunningTaskInfo
+ ): TransitionInfo =
+ TransitionInfo(type, 0 /* flags */).apply {
+ addChange(
+ TransitionInfo.Change(mock(), closingTaskLeash).apply {
+ mode = changeMode
+ parent = null
+ taskInfo = task
+ }
+ )
+ }
+
+ private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(windowingMode)
+ .build()
+}
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
new file mode 100644
index 0000000..2b60200
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WindowingMode
+import android.os.Handler
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
+import com.android.wm.shell.transition.Transitions
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for [DesktopMixedTransitionHandler]
+ *
+ * Usage: atest WMShellUnitTests:DesktopMixedTransitionHandlerTest
+ */
+@SmallTest
+@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopMixedTransitionHandlerTest : ShellTestCase() {
+
+ @Mock lateinit var transitions: Transitions
+ @Mock lateinit var desktopTaskRepository: DesktopModeTaskRepository
+ @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler
+ @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler
+ @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
+ @Mock lateinit var mockHandler: Handler
+ @Mock lateinit var closingTaskLeash: SurfaceControl
+
+ private lateinit var mixedHandler: DesktopMixedTransitionHandler
+
+ @Before
+ fun setUp() {
+ mixedHandler =
+ DesktopMixedTransitionHandler(
+ context,
+ transitions,
+ desktopTaskRepository,
+ freeformTaskTransitionHandler,
+ closeDesktopTaskTransitionHandler,
+ interactionJankMonitor,
+ mockHandler
+ )
+ }
+
+ @Test
+ fun startWindowingModeTransition_callsFreeformTaskTransitionHandler() {
+ val windowingMode = WINDOWING_MODE_FULLSCREEN
+ val wct = WindowContainerTransaction()
+
+ mixedHandler.startWindowingModeTransition(windowingMode, wct)
+
+ verify(freeformTaskTransitionHandler).startWindowingModeTransition(windowingMode, wct)
+ }
+
+ @Test
+ fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() {
+ val wct = WindowContainerTransaction()
+ whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any()))
+ .thenReturn(mock())
+
+ mixedHandler.startMinimizedModeTransition(wct)
+
+ verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct)
+ }
+
+ @Test
+ fun startRemoveTransition_startsCloseTransition() {
+ val wct = WindowContainerTransaction()
+
+ mixedHandler.startRemoveTransition(wct)
+
+ verify(transitions).startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler)
+ }
+
+ @Test
+ fun handleRequest_returnsNull() {
+ assertNull(mixedHandler.handleRequest(mock(), mock()))
+ }
+
+ @Test
+ fun startAnimation_withoutClosingDesktopTask_returnsFalse() {
+ val transition = mock<IBinder>()
+ val transitionInfo =
+ createTransitionInfo(
+ changeMode = WindowManager.TRANSIT_OPEN,
+ task = createTask(WINDOWING_MODE_FREEFORM)
+ )
+ whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()))
+ .thenReturn(true)
+
+ val started = mixedHandler.startAnimation(
+ transition = transition,
+ info = transitionInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertFalse("Should not start animation without closing desktop task", started)
+ }
+
+ @Test
+ fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() {
+ val transition = mock<IBinder>()
+ val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
+ whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(2)
+ whenever(
+ closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())
+ )
+ .thenReturn(true)
+
+ val started = mixedHandler.startAnimation(
+ transition = transition,
+ info = transitionInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ assertTrue("Should delegate animation to close transition handler", started)
+ verify(closeDesktopTaskTransitionHandler)
+ .startAnimation(eq(transition), eq(transitionInfo), any(), any(), any())
+ }
+
+ @Test
+ fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() {
+ val transition = mock<IBinder>()
+ val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
+ whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(1)
+ whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any()))
+ .thenReturn(mock())
+
+ mixedHandler.startAnimation(
+ transition = transition,
+ info = transitionInfo,
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ verify(transitions)
+ .dispatchTransition(
+ eq(transition),
+ eq(transitionInfo),
+ any(),
+ any(),
+ any(),
+ eq(mixedHandler)
+ )
+ verify(interactionJankMonitor)
+ .begin(
+ closingTaskLeash,
+ context,
+ mockHandler,
+ CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
+ )
+ }
+
+ private fun createTransitionInfo(
+ type: Int = WindowManager.TRANSIT_CLOSE,
+ changeMode: Int = WindowManager.TRANSIT_CLOSE,
+ task: RunningTaskInfo
+ ): TransitionInfo =
+ TransitionInfo(type, 0 /* flags */).apply {
+ addChange(
+ TransitionInfo.Change(mock(), closingTaskLeash).apply {
+ mode = changeMode
+ parent = null
+ taskInfo = task
+ }
+ )
+ }
+
+ private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(windowingMode)
+ .build()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index ca97229..6a5719b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -19,6 +19,8 @@
import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.wm.shell.EventLogTags
+import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
@@ -34,14 +36,15 @@
/**
* Tests for [DesktopModeEventLogger].
*/
-class DesktopModeEventLoggerTest {
+class DesktopModeEventLoggerTest : ShellTestCase() {
private val desktopModeEventLogger = DesktopModeEventLogger()
@JvmField
@Rule
val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
- .mockStatic(FrameworkStatsLog::class.java).build()!!
+ .mockStatic(FrameworkStatsLog::class.java)
+ .mockStatic(EventLogTags::class.java).build()!!
@Test
fun logSessionEnter_enterReason() = runBlocking {
@@ -60,6 +63,11 @@
eq(SESSION_ID)
)
}
+ verify {
+ EventLogTags.writeWmShellEnterDesktopMode(
+ eq(EnterReason.UNKNOWN_ENTER.reason),
+ eq(SESSION_ID))
+ }
}
@Test
@@ -79,6 +87,11 @@
eq(SESSION_ID)
)
}
+ verify {
+ EventLogTags.writeWmShellExitDesktopMode(
+ eq(ExitReason.UNKNOWN_EXIT.reason),
+ eq(SESSION_ID))
+ }
}
@Test
@@ -108,6 +121,22 @@
/* visible_task_count */
eq(TASK_COUNT))
}
+
+ verify {
+ EventLogTags.writeWmShellDesktopModeTaskUpdate(
+ eq(FrameworkStatsLog
+ .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED),
+ eq(TASK_UPDATE.instanceId),
+ eq(TASK_UPDATE.uid),
+ eq(TASK_UPDATE.taskHeight),
+ eq(TASK_UPDATE.taskWidth),
+ eq(TASK_UPDATE.taskX),
+ eq(TASK_UPDATE.taskY),
+ eq(SESSION_ID),
+ eq(UNSET_MINIMIZE_REASON),
+ eq(UNSET_UNMINIMIZE_REASON),
+ eq(TASK_COUNT))
+ }
}
@Test
@@ -137,6 +166,22 @@
/* visible_task_count */
eq(TASK_COUNT))
}
+
+ verify {
+ EventLogTags.writeWmShellDesktopModeTaskUpdate(
+ eq(FrameworkStatsLog
+ .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED),
+ eq(TASK_UPDATE.instanceId),
+ eq(TASK_UPDATE.uid),
+ eq(TASK_UPDATE.taskHeight),
+ eq(TASK_UPDATE.taskWidth),
+ eq(TASK_UPDATE.taskX),
+ eq(TASK_UPDATE.taskY),
+ eq(SESSION_ID),
+ eq(UNSET_MINIMIZE_REASON),
+ eq(UNSET_UNMINIMIZE_REASON),
+ eq(TASK_COUNT))
+ }
}
@Test
@@ -167,6 +212,22 @@
/* visible_task_count */
eq(TASK_COUNT))
}
+
+ verify {
+ EventLogTags.writeWmShellDesktopModeTaskUpdate(
+ eq(FrameworkStatsLog
+ .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED),
+ eq(TASK_UPDATE.instanceId),
+ eq(TASK_UPDATE.uid),
+ eq(TASK_UPDATE.taskHeight),
+ eq(TASK_UPDATE.taskWidth),
+ eq(TASK_UPDATE.taskX),
+ eq(TASK_UPDATE.taskY),
+ eq(SESSION_ID),
+ eq(UNSET_MINIMIZE_REASON),
+ eq(UNSET_UNMINIMIZE_REASON),
+ eq(TASK_COUNT))
+ }
}
@Test
@@ -200,6 +261,22 @@
/* visible_task_count */
eq(TASK_COUNT))
}
+
+ verify {
+ EventLogTags.writeWmShellDesktopModeTaskUpdate(
+ eq(FrameworkStatsLog
+ .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED),
+ eq(TASK_UPDATE.instanceId),
+ eq(TASK_UPDATE.uid),
+ eq(TASK_UPDATE.taskHeight),
+ eq(TASK_UPDATE.taskWidth),
+ eq(TASK_UPDATE.taskX),
+ eq(TASK_UPDATE.taskY),
+ eq(SESSION_ID),
+ eq(MinimizeReason.TASK_LIMIT.reason),
+ eq(UNSET_UNMINIMIZE_REASON),
+ eq(TASK_COUNT))
+ }
}
@Test
@@ -233,6 +310,22 @@
/* visible_task_count */
eq(TASK_COUNT))
}
+
+ verify {
+ EventLogTags.writeWmShellDesktopModeTaskUpdate(
+ eq(FrameworkStatsLog
+ .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED),
+ eq(TASK_UPDATE.instanceId),
+ eq(TASK_UPDATE.uid),
+ eq(TASK_UPDATE.taskHeight),
+ eq(TASK_UPDATE.taskWidth),
+ eq(TASK_UPDATE.taskX),
+ eq(TASK_UPDATE.taskY),
+ eq(SESSION_ID),
+ eq(UNSET_MINIMIZE_REASON),
+ eq(UnminimizeReason.TASKBAR_TAP.reason),
+ eq(TASK_COUNT))
+ }
}
companion object {
@@ -256,4 +349,4 @@
) = TaskUpdate(TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, minimizeReason,
unminimizeReason, TASK_COUNT)
}
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index bc40d89..97ceecc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -820,6 +820,18 @@
}
@Test
+ fun minimizeTask_withInvalidDisplay_minimizesCorrectTask() {
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 0)
+ repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 0)
+
+ repo.minimizeTask(displayId = INVALID_DISPLAY, taskId = 0)
+
+ assertThat(repo.isMinimizedTask(taskId = 0)).isTrue()
+ assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
+ }
+
+ @Test
fun unminimizeTask_unminimizesTask() {
repo.minimizeTask(displayId = 0, taskId = 0)
@@ -882,6 +894,51 @@
assertThat(tasks).containsExactly(1, 3).inOrder()
}
+ @Test
+ fun setTaskInFullImmersiveState_savedAsInImmersiveState() {
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
+
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+ }
+
+ @Test
+ fun removeTaskInFullImmersiveState_removedAsInImmersiveState() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = false)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
+ }
+
+ @Test
+ fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = false)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+ }
+
+ @Test
+ fun setTaskInFullImmersiveState_sameDisplay_overridesExistingFullImmersiveTask() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse()
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
+ }
+
+ @Test
+ fun setTaskInFullImmersiveState_differentDisplay_bothAreImmersive() {
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true)
+ repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true)
+
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue()
+ assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue()
+ }
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
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 8870846..2ddb1ac 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
@@ -1350,6 +1350,32 @@
}
@Test
+ fun moveTaskToFront_backgroundTask_launchesTask() {
+ val task = createTaskInfo(1)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+
+ controller.moveTaskToFront(task.taskId)
+
+ val wct = getLatestWct(type = TRANSIT_OPEN)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() {
+ val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+ val task = createTaskInfo(1001)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null)
+
+ controller.moveTaskToFront(task.taskId)
+
+ val wct = getLatestWct(type = TRANSIT_OPEN)
+ assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize
+ wct.assertReorderAt(0, freeformTasks[0], toTop = false)
+ wct.assertLaunchTaskAt(1, task.taskId, WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
fun moveToNextDisplay_noOtherDisplays() {
whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -2075,11 +2101,8 @@
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
- )
- fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -2112,8 +2135,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_backTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
+ fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
@@ -2122,11 +2144,8 @@
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
taskRepository.wallpaperActivityToken = MockToken().token()
@@ -2149,11 +2168,8 @@
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2165,7 +2181,7 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_multipleTasks_noBackNav_doesNotHandle() {
+ fun handleRequest_backTransition_multipleTasks_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2211,11 +2227,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_withBackNav_removesWallpaper() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
@@ -2231,11 +2244,8 @@
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -2244,22 +2254,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskNoToken_withWallpaper_withBackNav_removesTask() {
- val task = setUpFreeformTask()
-
- val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, task.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_singleTaskNoToken_noBackNav_doesNotHandle() {
+ fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() {
val task = setUpFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
@@ -2268,11 +2264,8 @@
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() {
val task = setUpFreeformTask()
taskRepository.wallpaperActivityToken = MockToken().token()
@@ -2282,26 +2275,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_singleTaskWithToken_removesWallpaperAndTask() {
- val task = setUpFreeformTask()
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_noBackNav_removesWallpaper() {
+ fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() {
val task = setUpFreeformTask()
val wallpaperToken = MockToken().token()
@@ -2313,11 +2288,8 @@
}
@Test
- @DisableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasks_noWallpaper_noBackNav_doesNotHandle() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2328,25 +2300,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasks_withWallpaper_withBackNav_removesTask() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
-
- taskRepository.wallpaperActivityToken = MockToken().token()
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
- assertNotNull(result, "Should handle request")
- result.assertRemoveAt(index = 0, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_multipleTasksFlagEnabled_noBackNav_doesNotHandle() {
+ fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() {
val task1 = setUpFreeformTask()
setUpFreeformTask()
@@ -2357,28 +2312,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() {
- val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_multipleTasksSingleNonClosing_noBackNav_removesWallpaper() {
+ fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
@@ -2392,28 +2327,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_multipleTasksOneNonMinimized_removesWallpaperAndTask() {
- val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val wallpaperToken = MockToken().token()
-
- taskRepository.wallpaperActivityToken = wallpaperToken
- taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
- val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
-
- // Should create remove wallpaper transaction
- assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken)
- result.assertRemoveAt(index = 1, task1.token)
- }
-
- @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
- fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_noBackNav_removesWallpaper() {
+ fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
@@ -2427,11 +2342,8 @@
}
@Test
- @EnableFlags(
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
- )
- fun handleRequest_closeTransition_minimizadTask_withWallpaper_withBackNav_removesWallpaper() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,)
+ fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val wallpaperToken = MockToken().token()
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 045e077..bdcb459 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
@@ -19,6 +19,8 @@
import android.app.ActivityManager.RunningTaskInfo
import android.os.Binder
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
@@ -33,6 +35,7 @@
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_MINIMIZE_WINDOW
import com.android.internal.jank.InteractionJankMonitor
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
@@ -243,6 +246,7 @@
}
@Test
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_activeNonMinimizedTasksStillAround_doesNothing() {
desktopTaskRepo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1)
desktopTaskRepo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2)
@@ -256,6 +260,7 @@
}
@Test
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() {
val wct = WindowContainerTransaction()
desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks(
@@ -265,6 +270,7 @@
}
@Test
+ @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_removesAllMinimizedTasks() {
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -283,6 +289,20 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_backNavEnabled_doesNothing() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
+ desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+
+ val wct = WindowContainerTransaction()
+ desktopTasksLimiter.leftoverMinimizedTasksRemover.onActiveTasksChanged(DEFAULT_DISPLAY)
+
+ assertThat(wct.hierarchyOps).isEmpty()
+ }
+
+ @Test
fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() {
(1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() }
@@ -291,7 +311,7 @@
desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
displayId = DEFAULT_DISPLAY,
wct = wct,
- newFrontTaskInfo = setUpFreeformTask())
+ newFrontTaskId = setUpFreeformTask().taskId)
assertThat(minimizedTaskId).isNull()
assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
@@ -307,7 +327,7 @@
desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
displayId = DEFAULT_DISPLAY,
wct = wct,
- newFrontTaskInfo = setUpFreeformTask())
+ newFrontTaskId = setUpFreeformTask().taskId)
assertThat(minimizedTaskId).isEqualTo(tasks.first())
assertThat(wct.hierarchyOps.size).isEqualTo(1)
@@ -325,7 +345,7 @@
desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
displayId = 0,
wct = wct,
- newFrontTaskInfo = setUpFreeformTask())
+ newFrontTaskId = setUpFreeformTask().taskId)
assertThat(minimizedTaskId).isNull()
assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
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 c989d16..42fcc83 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
@@ -18,11 +18,13 @@
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.platform.test.annotations.EnableFlags
import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.IWindowContainerToken
import android.window.TransitionInfo
@@ -110,6 +112,24 @@
verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ fun removeTasks_onTaskFullscreenLaunch_taskRemovedFromRepo() {
+ val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
+ whenever(taskRepository.getVisibleTaskCount(any())).thenReturn(1)
+ whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
+
+ transitionObserver.onTransitionReady(
+ transition = mock(),
+ info = createOpenTransition(task),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ )
+
+ verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
+ verify(taskRepository).removeFreeformTask(task.displayId, task.taskId)
+ }
+
private fun createBackNavigationTransition(
task: RunningTaskInfo?
): TransitionInfo {
@@ -125,11 +145,26 @@
}
}
- private fun createTaskInfo(id: Int) =
+ private fun createOpenTransition(
+ task: RunningTaskInfo?
+ ): TransitionInfo {
+ return TransitionInfo(TRANSIT_OPEN, 0 /* flags */).apply {
+ addChange(
+ Change(mock(), mock()).apply {
+ mode = TRANSIT_OPEN
+ parent = null
+ taskInfo = task
+ flags = flags
+ }
+ )
+ }
+ }
+
+ private fun createTaskInfo(id: Int, windowingMode: Int = WINDOWING_MODE_FREEFORM) =
RunningTaskInfo().apply {
taskId = id
displayId = DEFAULT_DISPLAY
- configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ configuration.windowConfiguration.windowingMode = windowingMode
token = WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java))
baseIntent = Intent().apply {
component = ComponentName("package", "component.name")
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 d9387d2..230f7e6 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
@@ -581,7 +581,7 @@
)
)
.thenReturn(token)
- handler.startDragToDesktopTransition(task.taskId, dragAnimator)
+ handler.startDragToDesktopTransition(task, dragAnimator)
return token
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index aad31a6..5596ad7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -54,6 +54,7 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
@@ -190,6 +191,35 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_appHandleExpanded_shouldMarkFeatureViewed() =
+ testScope.runTest {
+ setShouldShowAppHandleEducation(false)
+
+ // Simulate app handle visible and expanded.
+ testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true)
+ // Wait for some time before verifying
+ waitForBufferDelay()
+
+ verify(mockDataStoreRepository, times(1)).updateFeatureUsedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ fun init_showFirstTooltip_shouldMarkEducationViewed() =
+ testScope.runTest {
+ // App handle is visible. Should show education tooltip.
+ setShouldShowAppHandleEducation(true)
+
+ // Simulate app handle visible.
+ testCaptionStateFlow.value = createAppHandleState()
+ // Wait for first tooltip to showup.
+ waitForBufferDelay()
+
+ verify(mockDataStoreRepository, times(1)).updateEducationViewedTimestampMillis(eq(true))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() =
testScope.runTest {
// After first tooltip is dismissed, app handle is expanded. Should show second education
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
index 1c1c650..c286544 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -109,6 +109,24 @@
assertThat(result).isEqualTo(windowingEducationProto)
}
+ @Test
+ fun updateEducationViewedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateEducationViewedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasEducationViewedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
+ @Test
+ fun updateFeatureUsedTimestampMillis_updatesDatastoreProto() =
+ runTest(StandardTestDispatcher()) {
+ datastoreRepository.updateFeatureUsedTimestampMillis(true)
+
+ val result = testDatastore.data.first().hasFeatureUsedTimestampMillis()
+ assertThat(result).isEqualTo(true)
+ }
+
companion object {
private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb"
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 763d015..3b2c7e6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -18,15 +18,19 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.INVALID_DISPLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.platform.test.annotations.EnableFlags;
import android.view.SurfaceControl;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -139,6 +143,40 @@
verify(mLaunchAdjacentController).setLaunchAdjacentEnabled(true);
}
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ public void onTaskVanished_nonClosingTask_isMinimized() {
+ ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ task.isVisible = true;
+
+ mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+ task.isVisible = false;
+ task.displayId = INVALID_DISPLAY;
+ mFreeformTaskListener.onTaskVanished(task);
+
+ verify(mDesktopModeTaskRepository).minimizeTask(task.displayId, task.taskId);
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
+ public void onTaskVanished_closingTask_isNotMinimized() {
+ ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
+ task.isVisible = true;
+
+ mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl);
+
+ when(mDesktopModeTaskRepository.isClosingTask(task.taskId)).thenReturn(true);
+ task.isVisible = false;
+ task.displayId = INVALID_DISPLAY;
+ mFreeformTaskListener.onTaskVanished(task);
+
+ verify(mDesktopModeTaskRepository, never()).minimizeTask(task.displayId, task.taskId);
+ verify(mDesktopModeTaskRepository).removeFreeformTask(task.displayId, task.taskId);
+ }
+
@After
public void tearDown() {
mMockitoSession.finishMocking();
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 753d4cd..0364b51 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
@@ -22,6 +22,7 @@
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import static org.junit.Assert.assertEquals;
@@ -50,6 +51,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.platform.test.annotations.DisableFlags;
@@ -441,6 +443,40 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
+ public void testGetRecentTasks_hasDesktopTasks_persistenceEnabled_freeformTaskHaveBoundsSet() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+
+ t1.lastNonFullscreenBounds = new Rect(100, 200, 300, 400);
+ t2.lastNonFullscreenBounds = new Rect(150, 250, 350, 450);
+ setRawList(t1, t2);
+
+ when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isActiveTask(2)).thenReturn(true);
+
+ ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+ MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+
+ assertEquals(1, recentTasks.size());
+ GroupedRecentTaskInfo freeformGroup = recentTasks.get(0);
+
+ // Check bounds
+ assertEquals(t1.lastNonFullscreenBounds, freeformGroup.getTaskInfoList().get(
+ 0).configuration.windowConfiguration.getAppBounds());
+ assertEquals(t2.lastNonFullscreenBounds, freeformGroup.getTaskInfoList().get(
+ 1).configuration.windowConfiguration.getAppBounds());
+
+ // Check position in parent
+ assertEquals(new Point(t1.lastNonFullscreenBounds.left,
+ t1.lastNonFullscreenBounds.top),
+ freeformGroup.getTaskInfoList().get(0).positionInParent);
+ assertEquals(new Point(t2.lastNonFullscreenBounds.left,
+ t2.lastNonFullscreenBounds.top),
+ freeformGroup.getTaskInfoList().get(1).positionInParent);
+ }
+
+ @Test
public void testRemovedTaskRemovesSplit() {
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
@@ -623,6 +659,7 @@
private ActivityManager.RecentTaskInfo makeTaskInfo(int taskId) {
ActivityManager.RecentTaskInfo info = new ActivityManager.RecentTaskInfo();
info.taskId = taskId;
+ info.lastNonFullscreenBounds = new Rect();
return info;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 5f75423..ce482cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -30,7 +30,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
@@ -56,12 +55,11 @@
import android.os.Looper;
import android.os.UserHandle;
import android.testing.TestableContext;
-import android.view.IWindowSession;
import android.view.InsetsState;
import android.view.Surface;
import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
import android.view.WindowMetrics;
+import android.window.SnapshotDrawerUtils;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskSnapshot;
@@ -220,18 +218,10 @@
createWindowInfo(taskId, android.R.style.Theme, mBinder);
TaskSnapshot snapshot = createTaskSnapshot(100, 100, new Point(100, 100),
new Rect(0, 0, 0, 50), true /* hasImeSurface */);
- final IWindowSession session = WindowManagerGlobal.getWindowSession();
- spyOn(session);
- doReturn(WindowManagerGlobal.ADD_OKAY).when(session).addToDisplay(
- any() /* window */, any() /* attrs */,
- anyInt() /* viewVisibility */, anyInt() /* displayId */,
- anyInt() /* requestedVisibleTypes */, any() /* outInputChannel */,
- any() /* outInsetsState */, any() /* outActiveControls */,
- any() /* outAttachedFrame */, any() /* outSizeCompatScale */);
- TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo,
- mBinder,
- snapshot, mTestExecutor, () -> {
- });
+ final TaskSnapshotWindow mockSnapshotWindow = new TaskSnapshotWindow(
+ snapshot, SnapshotDrawerUtils.getOrCreateTaskDescription(windowInfo.taskInfo),
+ snapshot.getOrientation(),
+ () -> {}, mTestExecutor);
spyOn(mockSnapshotWindow);
try (AutoCloseable mockTaskSnapshotSession = new AutoCloseable() {
MockitoSession mockSession = mockitoSession()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
index d37b4cf..d63158c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -18,7 +18,7 @@
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -97,50 +97,38 @@
}
@Test
- public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException {
+ public void testOnlyDisplayChangeAffectsDisplayFocus() throws RemoteException {
final IBinder binder = mock(IBinder.class);
final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
- // Open a task on the default display, which doesn't change display focus because the
- // default display already has it.
+ // Open a task on the secondary display, but it doesn't change display focus because it only
+ // has a task change.
TransitionInfo info = mock(TransitionInfo.class);
final List<TransitionInfo.Change> changes = new ArrayList<>();
- setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+ setupTaskChange(changes, 123 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
true /* focused */);
when(info.getChanges()).thenReturn(changes);
mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
clearInvocations(mListener);
- // Open a new task on the secondary display and verify display focus changes to the display.
+ // Moving the secondary display to front must change display focus to it.
changes.clear();
- setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
- true /* focused */);
+ setupDisplayToTopChange(changes, SECONDARY_DISPLAY_ID);
when(info.getChanges()).thenReturn(changes);
mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
- verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
- clearInvocations(mListener);
+ verify(mListener, times(1))
+ .onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
- // Open the first task to front and verify display focus goes back to the default display.
+ // Moving the secondary display to front must change display focus back to it.
changes.clear();
- setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY,
- true /* focused */);
+ setupDisplayToTopChange(changes, DEFAULT_DISPLAY);
when(info.getChanges()).thenReturn(changes);
mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
- clearInvocations(mListener);
-
- // Open another task on the default display and verify no display focus switch as it's
- // already on the default display.
- changes.clear();
- setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
- true /* focused */);
- when(info.getChanges()).thenReturn(changes);
- mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
- verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY);
}
- private void setupChange(List<TransitionInfo.Change> changes, int taskId,
+ private void setupTaskChange(List<TransitionInfo.Change> changes, int taskId,
@TransitionMode int mode, int displayId, boolean focused) {
TransitionInfo.Change change = mock(TransitionInfo.Change.class);
RunningTaskInfo taskInfo = mock(RunningTaskInfo.class);
@@ -152,4 +140,12 @@
when(change.getMode()).thenReturn(mode);
changes.add(change);
}
+
+ private void setupDisplayToTopChange(List<TransitionInfo.Change> changes, int displayId) {
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(true);
+ when(change.hasFlags(FLAG_IS_DISPLAY)).thenReturn(true);
+ when(change.getEndDisplayId()).thenReturn(displayId);
+ changes.add(change);
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index d141c2d..0f16b9d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -47,6 +47,8 @@
taskInfo,
true,
false,
+ true /* isStatusBarVisible */,
+ false /* isKeyguardVisibleAndOccluded */,
InsetsState()
)
@@ -66,6 +68,8 @@
taskInfo,
true,
false,
+ true /* isStatusBarVisible */,
+ false /* isKeyguardVisibleAndOccluded */,
InsetsState()
)
@@ -81,6 +85,8 @@
taskInfo,
true,
false,
+ true /* isStatusBarVisible */,
+ false /* isKeyguardVisibleAndOccluded */,
InsetsState()
)
Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 9aa6a52..5ae4ca8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -27,6 +27,7 @@
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.Intent.ACTION_MAIN
import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.hardware.display.DisplayManager
@@ -86,6 +87,7 @@
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
@@ -159,6 +161,7 @@
@Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer
@Mock private lateinit var mockDisplayController: DisplayController
@Mock private lateinit var mockSplitScreenController: SplitScreenController
+ @Mock private lateinit var mockDesktopRepository: DesktopModeTaskRepository
@Mock private lateinit var mockDisplayLayout: DisplayLayout
@Mock private lateinit var displayInsetsController: DisplayInsetsController
@Mock private lateinit var mockSyncQueue: SyncTransactionQueue
@@ -230,6 +233,7 @@
mockShellCommandHandler,
mockWindowManager,
mockTaskOrganizer,
+ mockDesktopRepository,
mockDisplayController,
mockShellController,
displayInsetsController,
@@ -930,13 +934,13 @@
@Test
fun testDecor_onClickToOpenBrowser_closeMenus() {
val openInBrowserListenerCaptor = forClass(Consumer::class.java)
- as ArgumentCaptor<Consumer<Uri>>
+ as ArgumentCaptor<Consumer<Intent>>
val decor = createOpenTaskDecoration(
windowingMode = WINDOWING_MODE_FULLSCREEN,
onOpenInBrowserClickListener = openInBrowserListenerCaptor
)
- openInBrowserListenerCaptor.value.accept(Uri.EMPTY)
+ openInBrowserListenerCaptor.value.accept(Intent())
verify(decor).closeHandleMenu()
verify(decor).closeMaximizeMenu()
@@ -946,20 +950,19 @@
fun testDecor_onClickToOpenBrowser_opensBrowser() {
doNothing().whenever(spyContext).startActivity(any())
val uri = Uri.parse("https://www.google.com")
+ val intent = Intent(ACTION_MAIN, uri)
val openInBrowserListenerCaptor = forClass(Consumer::class.java)
- as ArgumentCaptor<Consumer<Uri>>
+ as ArgumentCaptor<Consumer<Intent>>
createOpenTaskDecoration(
windowingMode = WINDOWING_MODE_FULLSCREEN,
onOpenInBrowserClickListener = openInBrowserListenerCaptor
)
- openInBrowserListenerCaptor.value.accept(uri)
+ openInBrowserListenerCaptor.value.accept(intent)
verify(spyContext).startActivityAsUser(argThat { intent ->
- intent.data == uri
- && ((intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK) != 0)
- && intent.categories.contains(Intent.CATEGORY_LAUNCHER)
- && intent.action == Intent.ACTION_MAIN
+ uri.equals(intent.data)
+ && intent.action == ACTION_MAIN
}, eq(mockUserHandle))
}
@@ -1233,8 +1236,8 @@
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
onToSplitScreenClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
- onOpenInBrowserClickListener: ArgumentCaptor<Consumer<Uri>> =
- forClass(Consumer::class.java) as ArgumentCaptor<Consumer<Uri>>,
+ onOpenInBrowserClickListener: ArgumentCaptor<Consumer<Intent>> =
+ forClass(Consumer::class.java) as ArgumentCaptor<Consumer<Intent>>,
onCaptionButtonClickListener: ArgumentCaptor<View.OnClickListener> =
forClass(View.OnClickListener::class.java) as ArgumentCaptor<View.OnClickListener>,
onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> =
@@ -1296,8 +1299,8 @@
val decoration = mock(DesktopModeWindowDecoration::class.java)
whenever(
mockDesktopModeWindowDecorFactory.create(
- any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
- any(), any(), any(), any(), any(), any())
+ any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+ any(), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
whenever(decoration.isFocused).thenReturn(task.isFocused)
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 f007115..3e7f3bd 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
@@ -23,6 +23,7 @@
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;
+import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.statusBars;
import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
@@ -37,6 +38,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
@@ -53,9 +55,11 @@
import android.app.assist.AssistContent;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
@@ -102,6 +106,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.desktopmode.CaptionState;
+import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -124,6 +129,7 @@
import org.mockito.Mock;
import org.mockito.quality.Strictness;
+import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -157,6 +163,8 @@
@Mock
private ShellTaskOrganizer mMockShellTaskOrganizer;
@Mock
+ private DesktopModeTaskRepository mMockDesktopRepository;
+ @Mock
private Choreographer mMockChoreographer;
@Mock
private SyncTransactionQueue mMockSyncQueue;
@@ -187,7 +195,7 @@
@Mock
private Handler mMockHandler;
@Mock
- private Consumer<Uri> mMockOpenInBrowserClickListener;
+ private Consumer<Intent> mMockOpenInBrowserClickListener;
@Mock
private AppToWebGenericLinksParser mMockGenericLinksParser;
@Mock
@@ -242,9 +250,11 @@
when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any()))
.thenReturn(false);
when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel");
- final ActivityInfo activityInfo = new ActivityInfo();
- activityInfo.applicationInfo = new ApplicationInfo();
+ final ActivityInfo activityInfo = createActivityInfo();
when(mMockPackageManager.getActivityInfo(any(), anyInt())).thenReturn(activityInfo);
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = activityInfo;
+ when(mMockPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo);
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
@@ -284,7 +294,11 @@
DesktopModeWindowDecoration.updateRelayoutParams(
relayoutParams, mContext, taskInfo, /* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
}
@@ -300,7 +314,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mCornerRadius).isGreaterThan(0);
}
@@ -321,7 +339,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
}
@@ -343,7 +365,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
}
@@ -361,7 +387,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
}
@@ -378,7 +408,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -394,7 +428,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@@ -410,7 +448,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
}
@@ -427,7 +469,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -444,7 +490,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
@@ -462,7 +512,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue();
}
@@ -481,7 +535,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue();
}
@@ -498,7 +556,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0)
@@ -517,7 +579,11 @@
mTestableContext,
taskInfo,
/* applyStartTransactionOnDraw= */ true,
- /* shouldSetTaskPositionAndCrop */ false);
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
assertThat(
(relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0)
@@ -525,6 +591,171 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_addsPaddingInFullImmersive() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000));
+ final InsetsState insetsState = createInsetsState(List.of(
+ createInsetsSource(
+ 0 /* id */, statusBars(), true /* visible */, new Rect(0, 0, 1000, 50)),
+ createInsetsSource(
+ 1 /* id */, captionBar(), true /* visible */, new Rect(0, 0, 1000, 100))));
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ true,
+ insetsState);
+
+ // Takes status bar inset as padding, ignores caption bar inset.
+ assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_statusBarInvisible_captionVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ false,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ // Header is always shown because it's assumed the status bar is always visible.
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+ }
+
+ @Test
+ public void updateRelayoutParams_handle_statusBarVisibleAndNotOverKeyguard_captionVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+ }
+
+ @Test
+ public void updateRelayoutParams_handle_statusBarInvisible_captionNotVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ false,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
+ public void updateRelayoutParams_handle_overKeyguard_captionNotVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ true,
+ /* inFullImmersiveMode */ false,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_fullyImmersive_captionVisFollowsStatusBar() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ true,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isTrue();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ false,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ true,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void updateRelayoutParams_header_fullyImmersive_overKeyguard_captionNotVisible() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ true,
+ /* inFullImmersiveMode */ true,
+ new InsetsState());
+
+ assertThat(relayoutParams.mIsCaptionVisible).isFalse();
+ }
+
+ @Test
public void relayout_fullscreenTask_appliesTransactionImmediately() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -771,7 +1002,6 @@
// Verify handle menu's browser link is set to captured link since menu was opened before
// captured link expired
- createHandleMenu(decor);
verifyHandleMenuCreated(TEST_URI1);
}
@@ -782,7 +1012,7 @@
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, null /* web uri */,
null /* generic link */);
- final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor =
+ final ArgumentCaptor<Function1<Intent, Unit>> openInBrowserCaptor =
ArgumentCaptor.forClass(Function1.class);
// Simulate menu opening and clicking open in browser button
@@ -797,7 +1027,7 @@
any(),
any()
);
- openInBrowserCaptor.getValue().invoke(TEST_URI1);
+ openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
// Verify handle menu's browser link not set to captured link since link not valid after
// open in browser clicked
@@ -812,7 +1042,7 @@
final DesktopModeWindowDecoration decor = createWindowDecoration(
taskInfo, TEST_URI1 /* captured link */, null /* web uri */,
null /* generic link */);
- final ArgumentCaptor<Function1<Uri, Unit>> openInBrowserCaptor =
+ final ArgumentCaptor<Function1<Intent, Unit>> openInBrowserCaptor =
ArgumentCaptor.forClass(Function1.class);
createHandleMenu(decor);
verify(mMockHandleMenu).show(
@@ -826,9 +1056,10 @@
any()
);
- openInBrowserCaptor.getValue().invoke(TEST_URI1);
+ openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
- verify(mMockOpenInBrowserClickListener).accept(TEST_URI1);
+ verify(mMockOpenInBrowserClickListener).accept(
+ argThat(intent -> intent.getData() == TEST_URI1));
}
@Test
@@ -1021,8 +1252,9 @@
private void verifyHandleMenuCreated(@Nullable Uri uri) {
verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
- any(), anyBoolean(), anyBoolean(), anyBoolean(), eq(uri), anyInt(),
- anyInt(), anyInt());
+ any(), anyBoolean(), anyBoolean(), anyBoolean(),
+ argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)),
+ anyInt(), anyInt(), anyInt());
}
private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
@@ -1086,9 +1318,9 @@
boolean relayout) {
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
mContext, mMockDisplayController, mMockSplitScreenController,
- mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor,
- mMockChoreographer, mMockSyncQueue, mMockAppHeaderViewHolderFactory,
- mMockRootTaskDisplayAreaOrganizer,
+ mMockDesktopRepository, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
+ mMockHandler, mBgExecutor, mMockChoreographer, mMockSyncQueue,
+ mMockAppHeaderViewHolderFactory, mMockRootTaskDisplayAreaOrganizer,
mMockGenericLinksParser, mMockAssistContentRequester, SurfaceControl.Builder::new,
mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new,
new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory,
@@ -1128,19 +1360,39 @@
decor.onAssistContentReceived(mAssistContent);
}
+ private static ActivityInfo createActivityInfo() {
+ final ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.packageName = "DesktopModeWindowDecorationTestPackage";
+ final ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.applicationInfo = applicationInfo;
+ activityInfo.name = "DesktopModeWindowDecorationTest";
+ return activityInfo;
+ }
+
private static boolean hasNoInputChannelFeature(RelayoutParams params) {
return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL)
!= 0;
}
- private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
- final InsetsState state = new InsetsState();
- final InsetsSource source = new InsetsSource(/* id= */0, type);
+ private InsetsSource createInsetsSource(int id, @WindowInsets.Type.InsetsType int type,
+ boolean visible, @NonNull Rect frame) {
+ final InsetsSource source = new InsetsSource(id, type);
source.setVisible(visible);
- state.addSource(source);
+ source.setFrame(frame);
+ return source;
+ }
+
+ private InsetsState createInsetsState(@NonNull List<InsetsSource> sources) {
+ final InsetsState state = new InsetsState();
+ sources.forEach(state::addSource);
return state;
}
+ private InsetsState createInsetsState(@WindowInsets.Type.InsetsType int type, boolean visible) {
+ final InsetsSource source = createInsetsSource(0 /* id */, type, visible, new Rect());
+ return createInsetsState(List.of(source));
+ }
+
private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
View.OnGenericMotionListener, DragDetector.MotionEventHandler {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 2e117ac..94cabc4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -47,6 +47,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -246,6 +247,7 @@
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
@@ -319,6 +321,7 @@
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
@@ -571,11 +574,7 @@
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- assertTrue(mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, statusBars())
- .isVisible());
- assertTrue(mInsetsState.sourceSize() == 1);
- assertTrue(mInsetsState.sourceAt(0).getType() == statusBars());
-
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
@@ -623,33 +622,6 @@
}
@Test
- public void testRelayout_captionHidden_insetsRemoved() {
- final Display defaultDisplay = mock(Display.class);
- doReturn(defaultDisplay).when(mMockDisplayController)
- .getDisplay(Display.DEFAULT_DISPLAY);
-
- final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
- .setDisplayId(Display.DEFAULT_DISPLAY)
- .setVisible(true)
- .setBounds(new Rect(0, 0, 1000, 1000))
- .build();
- taskInfo.isFocused = true;
- // Caption visible at first.
- when(mMockDisplayController.getInsetsState(taskInfo.displayId))
- .thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
- windowDecor.relayout(taskInfo);
-
- // Hide caption so insets are removed.
- windowDecor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
-
- verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(captionBar()));
- verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(mandatorySystemGestures()));
- }
-
- @Test
public void testRelayout_captionHidden_neverWasVisible_insetsNotRemoved() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
@@ -661,9 +633,8 @@
.setBounds(new Rect(0, 0, 1000, 1000))
.build();
// Hidden from the beginning, so no insets were ever added.
- when(mMockDisplayController.getInsetsState(taskInfo.displayId))
- .thenReturn(createInsetsState(statusBars(), false /* visible */));
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = false;
windowDecor.relayout(taskInfo);
// Never added.
@@ -692,7 +663,7 @@
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
// Relayout will add insets.
- mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
+ mRelayoutParams.mIsCaptionVisible = true;
windowDecor.relayout(taskInfo);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()), any(), any(), anyInt());
@@ -740,6 +711,7 @@
final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
.setDisplayId(Display.DEFAULT_DISPLAY)
.setVisible(true);
+ mRelayoutParams.mIsCaptionVisible = true;
// Relayout twice with different bounds.
final ActivityManager.RunningTaskInfo firstTaskInfo =
@@ -767,6 +739,7 @@
final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
.setDisplayId(Display.DEFAULT_DISPLAY)
.setVisible(true);
+ mRelayoutParams.mIsCaptionVisible = true;
// Relayout twice with the same bounds.
final ActivityManager.RunningTaskInfo firstTaskInfo =
@@ -797,6 +770,7 @@
final ActivityManager.RunningTaskInfo taskInfo =
builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mIsCaptionVisible = true;
mRelayoutParams.mInsetSourceFlags =
FLAG_FORCE_CONSUMING | FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR;
windowDecor.relayout(taskInfo);
@@ -901,76 +875,61 @@
}
@Test
- public void onStatusBarVisibilityChange_fullscreen_shownToHidden_hidesCaption() {
+ public void onStatusBarVisibilityChange() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
decor.relayout(task);
- assertTrue(decor.mIsCaptionVisible);
+ assertTrue(decor.mIsStatusBarVisible);
decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
- assertFalse(decor.mIsCaptionVisible);
+ verify(decor, times(2)).relayout(task);
}
@Test
- public void onStatusBarVisibilityChange_fullscreen_hiddenToShown_showsCaption() {
+ public void onStatusBarVisibilityChange_noChange_doesNotRelayout() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
when(mMockDisplayController.getInsetsState(task.displayId))
- .thenReturn(createInsetsState(statusBars(), false /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
+ .thenReturn(createInsetsState(statusBars(), true /* visible */));
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
decor.relayout(task);
- assertFalse(decor.mIsCaptionVisible);
decor.onInsetsStateChanged(createInsetsState(statusBars(), true /* visible */));
- assertTrue(decor.mIsCaptionVisible);
+ verify(decor, times(1)).relayout(task);
}
@Test
- public void onStatusBarVisibilityChange_freeform_shownToHidden_keepsCaption() {
- final ActivityManager.RunningTaskInfo task = createTaskInfo();
- task.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
- when(mMockDisplayController.getInsetsState(task.displayId))
- .thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
- decor.relayout(task);
- assertTrue(decor.mIsCaptionVisible);
-
- decor.onInsetsStateChanged(createInsetsState(statusBars(), false /* visible */));
-
- assertTrue(decor.mIsCaptionVisible);
- }
-
- @Test
- public void onKeyguardStateChange_hiddenToShownAndOccluding_hidesCaption() {
+ public void onKeyguardStateChange() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
decor.relayout(task);
- assertTrue(decor.mIsCaptionVisible);
+ assertFalse(decor.mIsKeyguardVisibleAndOccluded);
decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
- assertFalse(decor.mIsCaptionVisible);
+ assertTrue(decor.mIsKeyguardVisibleAndOccluded);
+ verify(decor, times(2)).relayout(task);
}
@Test
- public void onKeyguardStateChange_showingAndOccludingToHidden_showsCaption() {
+ public void onKeyguardStateChange_noChange_doesNotRelayout() {
final ActivityManager.RunningTaskInfo task = createTaskInfo();
when(mMockDisplayController.getInsetsState(task.displayId))
.thenReturn(createInsetsState(statusBars(), true /* visible */));
- final TestWindowDecoration decor = createWindowDecoration(task);
- decor.onKeyguardStateChanged(true /* visible */, true /* occluding */);
- assertFalse(decor.mIsCaptionVisible);
+ final TestWindowDecoration decor = spy(createWindowDecoration(task));
+ decor.relayout(task);
+ assertFalse(decor.mIsKeyguardVisibleAndOccluded);
- decor.onKeyguardStateChanged(false /* visible */, false /* occluding */);
+ decor.onKeyguardStateChanged(false /* visible */, true /* occluding */);
- assertTrue(decor.mIsCaptionVisible);
+ verify(decor, times(1)).relayout(task);
}
private ActivityManager.RunningTaskInfo createTaskInfo() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
index 5594981..6749776 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
@@ -24,12 +24,16 @@
import android.testing.TestableLooper
import android.testing.TestableResources
import android.view.MotionEvent
+import android.view.Surface.ROTATION_180
+import android.view.Surface.ROTATION_90
import android.view.View
import android.view.WindowManager
import android.widget.TextView
+import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import com.android.wm.shell.R
import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipArrowDirection
import com.google.common.truth.Truth.assertThat
@@ -42,9 +46,11 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -52,6 +58,8 @@
class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() {
@Mock private lateinit var mockWindowManager: WindowManager
@Mock private lateinit var mockViewContainerFactory: AdditionalSystemViewContainer.Factory
+ @Mock private lateinit var mockDisplayController: DisplayController
+ @Mock private lateinit var mockPopupWindow: AdditionalSystemViewContainer
private lateinit var testableResources: TestableResources
private lateinit var testableContext: TestableContext
private lateinit var tooltipController: DesktopWindowingEducationTooltipController
@@ -69,7 +77,8 @@
Context.LAYOUT_INFLATER_SERVICE, context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
testableContext.addMockSystemService(WindowManager::class.java, mockWindowManager)
tooltipController =
- DesktopWindowingEducationTooltipController(testableContext, mockViewContainerFactory)
+ DesktopWindowingEducationTooltipController(
+ testableContext, mockViewContainerFactory, mockDisplayController)
}
@Test
@@ -218,6 +227,25 @@
verify(mockLambda).invoke()
}
+ @Test
+ fun showEducationTooltip_displayRotationChanged_hidesTooltip() {
+ whenever(
+ mockViewContainerFactory.create(any(), any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(mockPopupWindow)
+ val tooltipViewConfig = createTooltipConfig()
+
+ tooltipController.showEducationTooltip(tooltipViewConfig = tooltipViewConfig, taskId = 123)
+ tooltipController.onDisplayChange(
+ /* displayId= */ 123,
+ /* fromRotation= */ ROTATION_90,
+ /* toRotation= */ ROTATION_180,
+ /* newDisplayAreaInfo= */ null,
+ WindowContainerTransaction())
+
+ verify(mockPopupWindow, times(1)).releaseView()
+ verify(mockDisplayController, atLeastOnce()).removeDisplayChangingController(any())
+ }
+
private fun createTooltipConfig(
@LayoutRes tooltipViewLayout: Int = R.layout.desktop_windowing_education_top_arrow_tooltip,
tooltipViewGlobalCoordinates: Point = Point(0, 0),
diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt
index bb0fc41..bc269fe 100644
--- a/libs/appfunctions/api/current.txt
+++ b/libs/appfunctions/api/current.txt
@@ -16,7 +16,7 @@
ctor public AppFunctionService();
method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
- method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
+ method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>);
field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE";
field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService";
}
@@ -45,12 +45,12 @@
method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle);
field public static final String PROPERTY_RETURN_VALUE = "returnValue";
field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2
+ field public static final int RESULT_CANCELLED = 6; // 0x6
field public static final int RESULT_DENIED = 1; // 0x1
- field public static final int RESULT_DISABLED = 6; // 0x6
+ field public static final int RESULT_DISABLED = 5; // 0x5
field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3
field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4
field public static final int RESULT_OK = 0; // 0x0
- field public static final int RESULT_TIMED_OUT = 5; // 0x5
}
}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
index 6023c97..6e91de6 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java
@@ -26,6 +26,7 @@
import android.os.Binder;
import android.os.IBinder;
import android.os.CancellationSignal;
+import android.util.Log;
import java.util.function.Consumer;
@@ -143,7 +144,11 @@
*/
@MainThread
@Deprecated
- public abstract void onExecuteFunction(
+ public void onExecuteFunction(
@NonNull ExecuteAppFunctionRequest request,
- @NonNull Consumer<ExecuteAppFunctionResponse> callback);
+ @NonNull Consumer<ExecuteAppFunctionResponse> callback) {
+ Log.w(
+ "AppFunctionService",
+ "Calling deprecated default implementation of onExecuteFunction");
+ }
}
diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
index c7ce95b..d87fec79 100644
--- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
+++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java
@@ -73,11 +73,14 @@
*/
public static final int RESULT_INVALID_ARGUMENT = 4;
- /** The operation was timed out. */
- public static final int RESULT_TIMED_OUT = 5;
-
/** The caller tried to execute a disabled app function. */
- public static final int RESULT_DISABLED = 6;
+ public static final int RESULT_DISABLED = 5;
+
+ /**
+ * The operation was cancelled. Use this error code to report that a cancellation is done after
+ * receiving a cancellation signal.
+ */
+ public static final int RESULT_CANCELLED = 6;
/** The result code of the app function execution. */
@ResultCode private final int mResultCode;
@@ -236,7 +239,6 @@
RESULT_APP_UNKNOWN_ERROR,
RESULT_INTERNAL_ERROR,
RESULT_INVALID_ARGUMENT,
- RESULT_TIMED_OUT,
RESULT_DISABLED
})
@Retention(RetentionPolicy.SOURCE)
diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp
index 27add35..4305196 100644
--- a/libs/hwui/AutoBackendTextureRelease.cpp
+++ b/libs/hwui/AutoBackendTextureRelease.cpp
@@ -141,6 +141,13 @@
return;
}
+ if (!RenderThread::isCurrent()) {
+ // releaseQueueOwnership needs to run on RenderThread to prevent multithread calling
+ // setBackendTextureState will operate skia resource cache which need single owner
+ RenderThread::getInstance().queue().post([this, context]() { releaseQueueOwnership(context); });
+ return;
+ }
+
LOG_ALWAYS_FATAL_IF(Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan);
if (mBackendTexture.isValid()) {
// Passing in VK_IMAGE_LAYOUT_UNDEFINED means we keep the old layout.
diff --git a/libs/hwui/Gainmap.cpp b/libs/hwui/Gainmap.cpp
index 30f401e..ea955e2 100644
--- a/libs/hwui/Gainmap.cpp
+++ b/libs/hwui/Gainmap.cpp
@@ -15,12 +15,37 @@
*/
#include "Gainmap.h"
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkColorFilter.h>
+#include <SkImagePriv.h>
+#include <SkPaint.h>
+
+#include "HardwareBitmapUploader.h"
+
namespace android::uirenderer {
sp<Gainmap> Gainmap::allocateHardwareGainmap(const sp<Gainmap>& srcGainmap) {
auto gainmap = sp<Gainmap>::make();
gainmap->info = srcGainmap->info;
- const SkBitmap skSrcBitmap = srcGainmap->bitmap->getSkBitmap();
+ SkBitmap skSrcBitmap = srcGainmap->bitmap->getSkBitmap();
+ if (skSrcBitmap.info().colorType() == kAlpha_8_SkColorType &&
+ !HardwareBitmapUploader::hasAlpha8Support()) {
+ // The regular Bitmap::allocateHardwareBitmap will do a conversion that preserves channels,
+ // so alpha8 maps to the alpha channel of rgba. However, for gainmaps we will interpret
+ // the data of an rgba buffer differently as we'll only look at the rgb channels
+ // So we need to map alpha8 to rgbx_8888 essentially
+ SkBitmap bitmap;
+ bitmap.allocPixels(skSrcBitmap.info().makeColorType(kN32_SkColorType));
+ SkCanvas canvas(bitmap);
+ SkPaint paint;
+ const float alphaToOpaque[] = {0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
+ 0, 0, 0, 1, 0, 0, 0, 0, 0, 255};
+ paint.setColorFilter(SkColorFilters::Matrix(alphaToOpaque, SkColorFilters::Clamp::kNo));
+ canvas.drawImage(SkMakeImageFromRasterBitmap(skSrcBitmap, kNever_SkCopyPixelsMode), 0, 0,
+ SkSamplingOptions{}, &paint);
+ skSrcBitmap = bitmap;
+ }
sk_sp<Bitmap> skBitmap(Bitmap::allocateHardwareBitmap(skSrcBitmap));
if (!skBitmap.get()) {
return nullptr;
diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h
index 708f96e..7eb849f 100644
--- a/libs/hwui/hwui/Paint.h
+++ b/libs/hwui/hwui/Paint.h
@@ -159,6 +159,14 @@
return SkSamplingOptions(this->filterMode());
}
+ void setVariationOverride(minikin::VariationSettings&& varSettings) {
+ mFontVariationOverride = std::move(varSettings);
+ }
+
+ const minikin::VariationSettings& getFontVariationOverride() const {
+ return mFontVariationOverride;
+ }
+
// The Java flags (Paint.java) no longer fit into the native apis directly.
// These methods handle converting to and from them and the native representations
// in android::Paint.
@@ -179,6 +187,7 @@
float mLetterSpacing = 0;
float mWordSpacing = 0;
std::vector<minikin::FontFeature> mFontFeatureSettings;
+ minikin::VariationSettings mFontVariationOverride;
uint32_t mMinikinLocaleListId;
std::optional<minikin::FamilyVariant> mFamilyVariant;
uint32_t mHyphenEdit = 0;
diff --git a/libs/hwui/hwui/PaintImpl.cpp b/libs/hwui/hwui/PaintImpl.cpp
index c32ea01..6dfcedc 100644
--- a/libs/hwui/hwui/PaintImpl.cpp
+++ b/libs/hwui/hwui/PaintImpl.cpp
@@ -39,6 +39,7 @@
, mLetterSpacing(paint.mLetterSpacing)
, mWordSpacing(paint.mWordSpacing)
, mFontFeatureSettings(paint.mFontFeatureSettings)
+ , mFontVariationOverride(paint.mFontVariationOverride)
, mMinikinLocaleListId(paint.mMinikinLocaleListId)
, mFamilyVariant(paint.mFamilyVariant)
, mHyphenEdit(paint.mHyphenEdit)
@@ -59,6 +60,7 @@
mLetterSpacing = other.mLetterSpacing;
mWordSpacing = other.mWordSpacing;
mFontFeatureSettings = other.mFontFeatureSettings;
+ mFontVariationOverride = other.mFontVariationOverride;
mMinikinLocaleListId = other.mMinikinLocaleListId;
mFamilyVariant = other.mFamilyVariant;
mHyphenEdit = other.mHyphenEdit;
@@ -76,6 +78,7 @@
return static_cast<const SkPaint&>(a) == static_cast<const SkPaint&>(b) && a.mFont == b.mFont &&
a.mLooper == b.mLooper && a.mLetterSpacing == b.mLetterSpacing &&
a.mWordSpacing == b.mWordSpacing && a.mFontFeatureSettings == b.mFontFeatureSettings &&
+ a.mFontVariationOverride == b.mFontVariationOverride &&
a.mMinikinLocaleListId == b.mMinikinLocaleListId &&
a.mFamilyVariant == b.mFamilyVariant && a.mHyphenEdit == b.mHyphenEdit &&
a.mTypeface == b.mTypeface && a.mAlign == b.mAlign &&
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 286f06a..da23792 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -1127,6 +1127,36 @@
return leftMinikinPaint == rightMinikinPaint;
}
+ struct VariationBuilder {
+ std::vector<minikin::FontVariation> varSettings;
+ };
+
+ static jlong createFontVariationBuilder(CRITICAL_JNI_PARAMS_COMMA jint size) {
+ VariationBuilder* builder = new VariationBuilder();
+ builder->varSettings.reserve(size);
+ return reinterpret_cast<jlong>(builder);
+ }
+
+ static void addFontVariationToBuilder(CRITICAL_JNI_PARAMS_COMMA jlong builderPtr, jint tag,
+ jfloat value) {
+ VariationBuilder* builder = reinterpret_cast<VariationBuilder*>(builderPtr);
+ builder->varSettings.emplace_back(static_cast<minikin::AxisTag>(tag), value);
+ }
+
+ static void setFontVariationOverride(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle,
+ jlong builderPtr) {
+ Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+ if (builderPtr == 0) {
+ paint->setVariationOverride(minikin::VariationSettings());
+ return;
+ }
+
+ VariationBuilder* builder = reinterpret_cast<VariationBuilder*>(builderPtr);
+ paint->setVariationOverride(
+ minikin::VariationSettings(builder->varSettings, false /* sorted */));
+ delete builder;
+ }
+
}; // namespace PaintGlue
static const JNINativeMethod methods[] = {
@@ -1235,6 +1265,9 @@
{"nSetShadowLayer", "(JFFFJJ)V", (void*)PaintGlue::setShadowLayer},
{"nHasShadowLayer", "(J)Z", (void*)PaintGlue::hasShadowLayer},
{"nEqualsForTextMeasurement", "(JJ)Z", (void*)PaintGlue::equalsForTextMeasurement},
+ {"nCreateFontVariationBuilder", "(I)J", (void*)PaintGlue::createFontVariationBuilder},
+ {"nAddFontVariationToBuilder", "(JIF)V", (void*)PaintGlue::addFontVariationToBuilder},
+ {"nSetFontVariationOverride", "(JJ)V", (void*)PaintGlue::setFontVariationOverride},
};
int register_android_graphics_Paint(JNIEnv* env) {
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index f0ab6ec..0f24654 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -1089,7 +1089,24 @@
defaultRingtoneUri = ContentProvider.getUriWithoutUserId(defaultRingtoneUri);
if (defaultRingtoneUri == null) {
return -1;
- } else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
+ }
+
+ if (Flags.enableRingtoneHapticsCustomization()
+ && Utils.hasVibration(defaultRingtoneUri)) {
+ // skip to check TYPE_ALARM because the customized haptic hasn't enabled in alarm
+ if (defaultRingtoneUri.toString()
+ .contains(Settings.System.DEFAULT_RINGTONE_URI.toString())) {
+ return TYPE_RINGTONE;
+ } else if (defaultRingtoneUri.toString()
+ .contains(Settings.System.DEFAULT_NOTIFICATION_URI.toString())) {
+ return TYPE_NOTIFICATION;
+ } else if (defaultRingtoneUri.toString()
+ .contains(Settings.System.DEFAULT_ALARM_ALERT_URI.toString())) {
+ return TYPE_ALARM;
+ }
+ }
+
+ if (defaultRingtoneUri.equals(Settings.System.DEFAULT_RINGTONE_URI)) {
return TYPE_RINGTONE;
} else if (defaultRingtoneUri.equals(Settings.System.DEFAULT_NOTIFICATION_URI)) {
return TYPE_NOTIFICATION;
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index 9899e4e..83a4dd5 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -22,7 +22,6 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -57,8 +56,6 @@
}
};
- private static final String TAG = "RoutingSessionInfo";
-
private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
private static final String KEY_VOLUME_HANDLING = "volumeHandling";
@@ -142,15 +139,7 @@
mVolume = builder.mVolume;
mIsSystemSession = builder.mIsSystemSession;
-
- boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
- mVolumeHandling =
- defineVolumeHandling(
- mIsSystemSession,
- builder.mVolumeHandling,
- mSelectedRoutes,
- volumeAdjustmentForRemoteGroupSessions);
+ mVolumeHandling = builder.mVolumeHandling;
mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
mTransferReason = builder.mTransferReason;
@@ -207,20 +196,6 @@
return controlHints;
}
- @MediaRoute2Info.PlaybackVolume
- private static int defineVolumeHandling(
- boolean isSystemSession,
- @MediaRoute2Info.PlaybackVolume int volumeHandling,
- List<String> selectedRoutes,
- boolean volumeAdjustmentForRemoteGroupSessions) {
- if (!isSystemSession
- && !volumeAdjustmentForRemoteGroupSessions
- && selectedRoutes.size() > 1) {
- return MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
- }
- return volumeHandling;
- }
-
@NonNull
private static String ensureString(@Nullable String str) {
return str != null ? str : "";
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
index 3955ff0..5f5058d 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/RoutingSessionInfoTest.java
@@ -18,8 +18,6 @@
import static com.google.common.truth.Truth.assertThat;
-import android.content.res.Resources;
-import android.media.MediaRoute2Info;
import android.media.RoutingSessionInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -95,24 +93,4 @@
assertThat(sessionInfoWithProviderId2.getTransferableRoutes())
.isEqualTo(sessionInfoWithProviderId.getTransferableRoutes());
}
-
- @Test
- public void testGetVolumeHandlingGroupSession() {
- RoutingSessionInfo sessionInfo = new RoutingSessionInfo.Builder(
- TEST_ID, TEST_CLIENT_PACKAGE_NAME)
- .setName(TEST_NAME)
- .addSelectedRoute(TEST_ROUTE_ID_0)
- .addSelectedRoute(TEST_ROUTE_ID_2)
- .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
- .build();
-
- boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
-
- int expectedResult = volumeAdjustmentForRemoteGroupSessions
- ? MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE :
- MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
-
- assertThat(sessionInfo.getVolumeHandling()).isEqualTo(expectedResult);
- }
}
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index e7cb76c..96b7c13 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -223,6 +223,7 @@
field public static final String CATEGORY_PAYMENT = "payment";
field public static final String EXTRA_CATEGORY = "category";
field public static final String EXTRA_SERVICE_COMPONENT = "component";
+ field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT = 3; // 0x3
field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DH = 0; // 0x0
field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE = 1; // 0x1
field @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC = 2; // 0x2
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 94231b0..4428ade 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -58,12 +58,16 @@
@FlaggedApi("android.nfc.nfc_oem_extension") public final class NfcOemExtension {
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void clearPreference();
method @FlaggedApi("android.nfc.nfc_oem_extension") @NonNull public java.util.List<java.lang.String> getActiveNfceeList();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @NonNull @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public android.nfc.RoutingStatus getRoutingStatus();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean hasUserEnabledNfc();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isAutoChangeEnabled();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagPresent();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void pausePolling(int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void resumePolling();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setAutoChangeEnabled(boolean);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOnMode(int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void triggerInitialization();
@@ -90,7 +94,11 @@
method public void onEnable(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onEnableFinished(int);
method public void onEnableStarted();
+ method public void onGetOemAppSearchIntent(@NonNull java.util.List<java.lang.String>, @NonNull java.util.function.Consumer<android.content.Intent>);
method public void onHceEventReceived(int);
+ method public void onLaunchHceAppChooserActivity(@NonNull String, @NonNull java.util.List<android.nfc.cardemulation.ApduServiceInfo>, @NonNull android.content.ComponentName, @NonNull String);
+ method public void onLaunchHceTapAgainDialog(@NonNull android.nfc.cardemulation.ApduServiceInfo, @NonNull String);
+ method public void onNdefMessage(@NonNull android.nfc.Tag, @NonNull android.nfc.NdefMessage, @NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onNdefRead(@NonNull java.util.function.Consumer<java.lang.Boolean>);
method public void onReaderOptionChanged(boolean);
method public void onRfDiscoveryStarted(boolean);
@@ -101,6 +109,12 @@
method public void onTagDispatch(@NonNull java.util.function.Consumer<java.lang.Boolean>);
}
+ @FlaggedApi("android.nfc.nfc_oem_extension") public class RoutingStatus {
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultIsoDepRoute();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultOffHostRoute();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int getDefaultRoute();
+ }
+
}
package android.nfc.cardemulation {
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index 19b9e0f..1eae3c6 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -51,4 +51,8 @@
void overrideRoutingTable(int userHandle, String protocol, String technology, in String pkg);
void recoverRoutingTable(int userHandle);
boolean isEuiccSupported();
+ void setAutoChangeStatus(boolean state);
+ boolean isAutoChangeEnabled();
+ List<String> getRoutingStatus();
+ void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech);
}
diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
index e49ef7e..48c7ee6 100644
--- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
+++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl
@@ -15,9 +15,14 @@
*/
package android.nfc;
+import android.content.ComponentName;
+import android.nfc.cardemulation.ApduServiceInfo;
+import android.nfc.NdefMessage;
import android.nfc.Tag;
import android.os.ResultReceiver;
+import java.util.List;
+
/**
* @hide
*/
@@ -41,4 +46,8 @@
void onCardEmulationActivated(boolean isActivated);
void onRfFieldActivated(boolean isActivated);
void onRfDiscoveryStarted(boolean isDiscoveryStarted);
+ void onGetOemAppSearchIntent(in List<String> firstPackage, in ResultReceiver intentConsumer);
+ void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent);
+ void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category);
+ void onLaunchHceTapAgainActivity(in ApduServiceInfo service, in String category);
}
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 6d5c069..fb63b5c 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -16,6 +16,12 @@
package android.nfc;
+import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_DH;
+import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE;
+import static android.nfc.cardemulation.CardEmulation.PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC;
+import static android.nfc.cardemulation.CardEmulation.routeIntToString;
+
+import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -23,8 +29,14 @@
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.nfc.cardemulation.ApduServiceInfo;
+import android.nfc.cardemulation.CardEmulation;
+import android.nfc.cardemulation.CardEmulation.ProtocolAndTechnologyRoute;
import android.os.Binder;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.util.Log;
@@ -306,6 +318,60 @@
* @param isDiscoveryStarted true, if RF discovery started, else RF state is Idle.
*/
void onRfDiscoveryStarted(boolean isDiscoveryStarted);
+
+ /**
+ * Gets the intent to find the OEM package in the OEM App market. If the consumer returns
+ * {@code null} or a timeout occurs, the intent from the first available package will be
+ * used instead.
+ *
+ * @param packages the OEM packages name stored in the tag
+ * @param intentConsumer The {@link Consumer} to be completed.
+ * The {@link Consumer#accept(Object)} should be called with
+ * the Intent required.
+ *
+ */
+ void onGetOemAppSearchIntent(@NonNull List<String> packages,
+ @NonNull Consumer<Intent> intentConsumer);
+
+ /**
+ * Checks if the NDEF message contains any specific OEM package executable content
+ *
+ * @param tag the {@link android.nfc.Tag Tag}
+ * @param message NDEF Message to read from tag
+ * @param hasOemExecutableContent The {@link Consumer} to be completed. If there is
+ * OEM package executable content, the
+ * {@link Consumer#accept(Object)} should be called with
+ * {@link Boolean#TRUE}, otherwise call with
+ * {@link Boolean#FALSE}.
+ */
+ void onNdefMessage(@NonNull Tag tag, @NonNull NdefMessage message,
+ @NonNull Consumer<Boolean> hasOemExecutableContent);
+
+ /**
+ * Callback to indicate the app chooser activity should be launched for handling CE
+ * transaction. This is invoked for example when there are more than 1 app installed that
+ * can handle the HCE transaction. OEMs can launch the Activity based
+ * on their requirement.
+ *
+ * @param selectedAid the selected AID from APDU
+ * @param services {@link ApduServiceInfo} of the service triggering the activity
+ * @param failedComponent the component failed to be resolved
+ * @param category the category of the service
+ */
+ void onLaunchHceAppChooserActivity(@NonNull String selectedAid,
+ @NonNull List<ApduServiceInfo> services,
+ @NonNull ComponentName failedComponent,
+ @NonNull String category);
+
+ /**
+ * Callback to indicate tap again dialog should be launched for handling HCE transaction.
+ * This is invoked for example when a CE service needs the device to unlocked before
+ * handling the transaction. OEMs can launch the Activity based on their requirement.
+ *
+ * @param service {@link ApduServiceInfo} of the service triggering the dialog
+ * @param category the category of the service
+ */
+ void onLaunchHceTapAgainDialog(@NonNull ApduServiceInfo service, @NonNull String category);
}
@@ -523,6 +589,85 @@
NfcAdapter.callService(() -> NfcAdapter.sService.resumePolling());
}
+ /**
+ * Set whether to enable auto routing change or not (enabled by default).
+ * If disabled, routing targets are limited to a single off-host destination.
+ *
+ * @param state status of auto routing change, true if enable, otherwise false
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public void setAutoChangeEnabled(boolean state) {
+ NfcAdapter.callService(() ->
+ NfcAdapter.sCardEmulationService.setAutoChangeStatus(state));
+ }
+
+ /**
+ * Check if auto routing change is enabled or not.
+ *
+ * @return true if enabled, otherwise false
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public boolean isAutoChangeEnabled() {
+ return NfcAdapter.callServiceReturn(() ->
+ NfcAdapter.sCardEmulationService.isAutoChangeEnabled(), false);
+ }
+
+ /**
+ * Get current routing status
+ *
+ * @return {@link RoutingStatus} indicating the default route, default ISO-DEP
+ * route and default off-host route.
+ */
+ @NonNull
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public RoutingStatus getRoutingStatus() {
+ List<String> status = NfcAdapter.callServiceReturn(() ->
+ NfcAdapter.sCardEmulationService.getRoutingStatus(), new ArrayList<>());
+ return new RoutingStatus(routeStringToInt(status.get(0)),
+ routeStringToInt(status.get(1)),
+ routeStringToInt(status.get(2)));
+ }
+
+ /**
+ * Overwrites NFC controller routing table, which includes Protocol Route, Technology Route,
+ * and Empty AID Route.
+ *
+ * The parameter set to
+ * {@link ProtocolAndTechnologyRoute#PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET}
+ * can be used to keep current values for that entry. At least one route should be overridden
+ * when calling this API, otherwise throw {@link IllegalArgumentException}.
+ *
+ * @param protocol ISO-DEP route destination, where the possible inputs are defined in
+ * {@link ProtocolAndTechnologyRoute}.
+ * @param technology Tech-A, Tech-B and Tech-F route destination, where the possible inputs
+ * are defined in
+ * {@link ProtocolAndTechnologyRoute}
+ * @param emptyAid Zero-length AID route destination, where the possible inputs are defined in
+ * {@link ProtocolAndTechnologyRoute}
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ public void overwriteRoutingTable(
+ @CardEmulation.ProtocolAndTechnologyRoute int protocol,
+ @CardEmulation.ProtocolAndTechnologyRoute int technology,
+ @CardEmulation.ProtocolAndTechnologyRoute int emptyAid) {
+
+ String protocolRoute = routeIntToString(protocol);
+ String technologyRoute = routeIntToString(technology);
+ String emptyAidRoute = routeIntToString(emptyAid);
+
+ NfcAdapter.callService(() ->
+ NfcAdapter.sCardEmulationService.overwriteRoutingTable(
+ mContext.getUser().getIdentifier(),
+ emptyAidRoute,
+ protocolRoute,
+ technologyRoute
+ ));
+ }
+
private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {
@Override
@@ -562,25 +707,25 @@
public void onApplyRouting(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isSkipped), cb::onApplyRouting, ex));
+ new ReceiverWrapper<>(isSkipped), cb::onApplyRouting, ex));
}
@Override
public void onNdefRead(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isSkipped), cb::onNdefRead, ex));
+ new ReceiverWrapper<>(isSkipped), cb::onNdefRead, ex));
}
@Override
public void onEnable(ResultReceiver isAllowed) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isAllowed), cb::onEnable, ex));
+ new ReceiverWrapper<>(isAllowed), cb::onEnable, ex));
}
@Override
public void onDisable(ResultReceiver isAllowed) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isAllowed), cb::onDisable, ex));
+ new ReceiverWrapper<>(isAllowed), cb::onDisable, ex));
}
@Override
public void onBootStarted() throws RemoteException {
@@ -616,7 +761,7 @@
public void onTagDispatch(ResultReceiver isSkipped) throws RemoteException {
mCallbackMap.forEach((cb, ex) ->
handleVoidCallback(
- new ReceiverWrapper(isSkipped), cb::onTagDispatch, ex));
+ new ReceiverWrapper<>(isSkipped), cb::onTagDispatch, ex));
}
@Override
public void onRoutingChanged() throws RemoteException {
@@ -635,6 +780,59 @@
handleVoidCallback(enabled, cb::onReaderOptionChanged, ex));
}
+ @Override
+ public void onGetOemAppSearchIntent(List<String> packages, ResultReceiver intentConsumer)
+ throws RemoteException {
+ mCallbackMap.forEach((cb, ex) ->
+ handleVoid2ArgCallback(packages, new ReceiverWrapper<>(intentConsumer),
+ cb::onGetOemAppSearchIntent, ex));
+ }
+
+ @Override
+ public void onNdefMessage(Tag tag, NdefMessage message,
+ ResultReceiver hasOemExecutableContent) throws RemoteException {
+ mCallbackMap.forEach((cb, ex) -> {
+ synchronized (mLock) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ ex.execute(() -> cb.onNdefMessage(
+ tag, message, new ReceiverWrapper<>(hasOemExecutableContent)));
+ } catch (RuntimeException exception) {
+ throw exception;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onLaunchHceAppChooserActivity(String selectedAid,
+ List<ApduServiceInfo> services,
+ ComponentName failedComponent, String category)
+ throws RemoteException {
+ mCallbackMap.forEach((cb, ex) -> {
+ synchronized (mLock) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ ex.execute(() -> cb.onLaunchHceAppChooserActivity(
+ selectedAid, services, failedComponent, category));
+ } catch (RuntimeException exception) {
+ throw exception;
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onLaunchHceTapAgainActivity(ApduServiceInfo service, String category)
+ throws RemoteException {
+ mCallbackMap.forEach((cb, ex) ->
+ handleVoid2ArgCallback(service, category, cb::onLaunchHceTapAgainDialog, ex));
+ }
+
private <T> void handleVoidCallback(
T input, Consumer<T> callbackMethod, Executor executor) {
synchronized (mLock) {
@@ -718,7 +916,16 @@
}
}
- private class ReceiverWrapper implements Consumer<Boolean> {
+ private @CardEmulation.ProtocolAndTechnologyRoute int routeStringToInt(String route) {
+ return switch (route) {
+ case "DH" -> PROTOCOL_AND_TECHNOLOGY_ROUTE_DH;
+ case "eSE" -> PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE;
+ case "SIM" -> PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC;
+ default -> throw new IllegalStateException("Unexpected value: " + route);
+ };
+ }
+
+ private class ReceiverWrapper<T> implements Consumer<T> {
private final ResultReceiver mResultReceiver;
ReceiverWrapper(ResultReceiver resultReceiver) {
@@ -726,12 +933,19 @@
}
@Override
- public void accept(Boolean result) {
- mResultReceiver.send(result ? 1 : 0, null);
+ public void accept(T result) {
+ if (result instanceof Boolean) {
+ mResultReceiver.send((Boolean) result ? 1 : 0, null);
+ } else if (result instanceof Intent) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable("intent", (Intent) result);
+ mResultReceiver.send(0, bundle);
+ }
+
}
@Override
- public Consumer<Boolean> andThen(Consumer<? super Boolean> after) {
+ public Consumer<T> andThen(Consumer<? super T> after) {
return Consumer.super.andThen(after);
}
}
diff --git a/nfc/java/android/nfc/RoutingStatus.java b/nfc/java/android/nfc/RoutingStatus.java
new file mode 100644
index 0000000..4a1b1f3
--- /dev/null
+++ b/nfc/java/android/nfc/RoutingStatus.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.nfc;
+
+import android.annotation.FlaggedApi;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.nfc.cardemulation.CardEmulation;
+
+/**
+ * A class indicating default route, ISO-DEP route and off-host route.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+public class RoutingStatus {
+ private final @CardEmulation.ProtocolAndTechnologyRoute int mDefaultRoute;
+ private final @CardEmulation.ProtocolAndTechnologyRoute int mDefaultIsoDepRoute;
+ private final @CardEmulation.ProtocolAndTechnologyRoute int mDefaultOffHostRoute;
+
+ RoutingStatus(@CardEmulation.ProtocolAndTechnologyRoute int mDefaultRoute,
+ @CardEmulation.ProtocolAndTechnologyRoute int mDefaultIsoDepRoute,
+ @CardEmulation.ProtocolAndTechnologyRoute int mDefaultOffHostRoute) {
+ this.mDefaultRoute = mDefaultRoute;
+ this.mDefaultIsoDepRoute = mDefaultIsoDepRoute;
+ this.mDefaultOffHostRoute = mDefaultOffHostRoute;
+ }
+
+ /**
+ * Getter of the default route.
+ * @return an integer defined in
+ * {@link android.nfc.cardemulation.CardEmulation.ProtocolAndTechnologyRoute}
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @CardEmulation.ProtocolAndTechnologyRoute
+ public int getDefaultRoute() {
+ return mDefaultRoute;
+ }
+
+ /**
+ * Getter of the default ISO-DEP route.
+ * @return an integer defined in
+ * {@link android.nfc.cardemulation.CardEmulation.ProtocolAndTechnologyRoute}
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @CardEmulation.ProtocolAndTechnologyRoute
+ public int getDefaultIsoDepRoute() {
+ return mDefaultIsoDepRoute;
+ }
+
+ /**
+ * Getter of the default off-host route.
+ * @return an integer defined in
+ * {@link android.nfc.cardemulation.CardEmulation.ProtocolAndTechnologyRoute}
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ @CardEmulation.ProtocolAndTechnologyRoute
+ public int getDefaultOffHostRoute() {
+ return mDefaultOffHostRoute;
+ }
+
+}
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index 4be082c..d8f04c5 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -168,6 +168,12 @@
public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC = 2;
/**
+ * Route to the default value in config file.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
+ public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT = 3;
+
+ /**
* Route unset.
*/
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
@@ -895,45 +901,47 @@
PROTOCOL_AND_TECHNOLOGY_ROUTE_DH,
PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE,
PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC,
- PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET
+ PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET,
+ PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT
})
@Retention(RetentionPolicy.SOURCE)
public @interface ProtocolAndTechnologyRoute {}
- /**
- * Setting NFC controller routing table, which includes Protocol Route and Technology Route,
- * while this Activity is in the foreground.
- *
- * The parameter set to {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET}
- * can be used to keep current values for that entry. Either
- * Protocol Route or Technology Route should be override when calling this API, otherwise
- * throw {@link IllegalArgumentException}.
- * <p>
- * Example usage in an Activity that requires to set proto route to "ESE" and keep tech route:
- * <pre>
- * protected void onResume() {
- * mNfcAdapter.overrideRoutingTable(
- * this, {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE}, null);
- * }</pre>
- * </p>
- * Also activities must call {@link #recoverRoutingTable(Activity)}
- * when it goes to the background. Only the package of the
- * currently preferred service (the service set as preferred by the current foreground
- * application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the
- * current Default Wallet Role Holder {@link RoleManager#ROLE_WALLET}),
- * otherwise a call to this method will fail and throw {@link SecurityException}.
- * @param activity The Activity that requests NFC controller routing table to be changed.
- * @param protocol ISO-DEP route destination, where the possible inputs are defined
- * in {@link ProtocolAndTechnologyRoute}.
- * @param technology Tech-A, Tech-B and Tech-F route destination, where the possible inputs
- * are defined in {@link ProtocolAndTechnologyRoute}
- * @throws SecurityException if the caller is not the preferred NFC service
- * @throws IllegalArgumentException if the activity is not resumed or the caller is not in the
- * foreground.
- * <p>
- * This is a high risk API and only included to support mainline effort
- * @hide
- */
+ /**
+ * Setting NFC controller routing table, which includes Protocol Route and Technology Route,
+ * while this Activity is in the foreground.
+ *
+ * The parameter set to {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET}
+ * can be used to keep current values for that entry. Either
+ * Protocol Route or Technology Route should be override when calling this API, otherwise
+ * throw {@link IllegalArgumentException}.
+ * <p>
+ * Example usage in an Activity that requires to set proto route to "ESE" and keep tech route:
+ * <pre>
+ * protected void onResume() {
+ * mNfcAdapter.overrideRoutingTable(
+ * this, {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE},
+ * {@link #PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET});
+ * }</pre>
+ * </p>
+ * Also activities must call {@link #recoverRoutingTable(Activity)}
+ * when it goes to the background. Only the package of the
+ * currently preferred service (the service set as preferred by the current foreground
+ * application via {@link CardEmulation#setPreferredService(Activity, ComponentName)} or the
+ * current Default Wallet Role Holder {@link RoleManager#ROLE_WALLET}),
+ * otherwise a call to this method will fail and throw {@link SecurityException}.
+ * @param activity The Activity that requests NFC controller routing table to be changed.
+ * @param protocol ISO-DEP route destination, where the possible inputs are defined
+ * in {@link ProtocolAndTechnologyRoute}.
+ * @param technology Tech-A, Tech-B and Tech-F route destination, where the possible inputs
+ * are defined in {@link ProtocolAndTechnologyRoute}
+ * @throws SecurityException if the caller is not the preferred NFC service
+ * @throws IllegalArgumentException if the activity is not resumed or the caller is not in the
+ * foreground.
+ * <p>
+ * This is a high risk API and only included to support mainline effort
+ * @hide
+ */
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE)
public void overrideRoutingTable(
@@ -942,26 +950,14 @@
if (!activity.isResumed()) {
throw new IllegalArgumentException("Activity must be resumed.");
}
- String protocolRoute = switch (protocol) {
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_DH -> "DH";
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE -> "ESE";
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC -> "UICC";
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET -> null;
- default -> throw new IllegalStateException("Unexpected value: " + protocol);
- };
- String technologyRoute = switch (technology) {
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_DH -> "DH";
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE -> "ESE";
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC -> "UICC";
- case PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET -> null;
- default -> throw new IllegalStateException("Unexpected value: " + protocol);
- };
+ String protocolRoute = routeIntToString(protocol);
+ String technologyRoute = routeIntToString(technology);
callService(() ->
sService.overrideRoutingTable(
- mContext.getUser().getIdentifier(),
- protocolRoute,
- technologyRoute,
- mContext.getPackageName()));
+ mContext.getUser().getIdentifier(),
+ protocolRoute,
+ technologyRoute,
+ mContext.getPackageName()));
}
/**
@@ -1068,4 +1064,16 @@
}
return defaultReturn;
}
+
+ /** @hide */
+ public static String routeIntToString(@ProtocolAndTechnologyRoute int route) {
+ return switch (route) {
+ case PROTOCOL_AND_TECHNOLOGY_ROUTE_DH -> "DH";
+ case PROTOCOL_AND_TECHNOLOGY_ROUTE_ESE -> "eSE";
+ case PROTOCOL_AND_TECHNOLOGY_ROUTE_UICC -> "SIM";
+ case PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET -> null;
+ case PROTOCOL_AND_TECHNOLOGY_ROUTE_DEFAULT -> "default";
+ default -> throw new IllegalStateException("Unexpected value: " + route);
+ };
+ }
}
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index a57d6eb..b266912 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -50,13 +50,13 @@
<!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
<!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] -->
- <string name="title_app_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream your <xliff:g id="device_type" example="phone">%2$s</xliff:g>\u2019s apps to <strong><xliff:g id="device_name" example="Chromebook">%3$s</xliff:g></strong>?</string>
+ <string name="title_app_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream your <xliff:g id="device_type" example="phone">%2$s</xliff:g>\u2019s apps and system features to <strong><xliff:g id="device_name" example="Chromebook">%3$s</xliff:g></strong>?</string>
<!-- Summary for associating an application with a companion device of APP_STREAMING profile [CHAR LIMIT=NONE] -->
- <string name="summary_app_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will have access to anything that’s visible or played on the <xliff:g id="device_type" example="phone">%2$s</xliff:g>, including audio, photos, passwords, and messages.<br/><br/><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will be able to stream apps to <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g> until you remove access to this permission.</string>
+ <string name="summary_app_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will have access to anything that’s visible or played on your <xliff:g id="device_type" example="phone">%2$s</xliff:g>, including audio, photos, payment info, passwords, and messages.<br/><br/><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will be able to stream apps to <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g> until you remove access to this permission.</string>
<!-- Description of the helper dialog for APP_STREAMING profile. [CHAR LIMIT=NONE] -->
- <string name="helper_summary_app_streaming"><xliff:g id="app_name" example="GMS">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_name" example="Chromebook">%2$s</xliff:g> to display and stream apps between your devices</string>
+ <string name="helper_summary_app_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> is requesting permission on behalf of <xliff:g id="device_name" example="Chromebook">%2$s</xliff:g> to stream apps and system features from your <xliff:g id="device_type" example="phone">%3$s</xliff:g></string>
<!-- ================= DEVICE_PROFILE_AUTOMOTIVE_PROJECTION ================= -->
@@ -80,13 +80,13 @@
<!-- ================= DEVICE_PROFILE_NEARBY_DEVICE_STREAMING ================= -->
<!-- Confirmation for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile (type) [CHAR LIMIT=NONE] -->
- <string name="title_nearby_device_streaming">Allow <strong><xliff:g id="device_name" example="NearbyStreamer">%1$s</xliff:g></strong> to stream your <xliff:g id="device_type" example="phone">%2$s</xliff:g>\u2019s apps and system features to <strong><xliff:g id="device_name" example="Chromebook">%3$s</xliff:g></strong>?</string>
+ <string name="title_nearby_device_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to stream your <xliff:g id="device_type" example="phone">%2$s</xliff:g>\u2019s apps to <strong><xliff:g id="device_name" example="Chromebook">%3$s</xliff:g></strong>?</string>
<!-- Summary for associating an application with a companion device of NEARBY_DEVICE_STREAMING profile [CHAR LIMIT=NONE] -->
- <string name="summary_nearby_device_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will have access to anything that’s visible or played on your <xliff:g id="device_type" example="phone">%2$s</xliff:g>, including audio, photos, payment info, passwords, and messages.<br/><br/><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will be able to stream apps and system features to <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g> until you remove access to this permission.</string>
+ <string name="summary_nearby_device_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will have access to anything that’s visible or played on <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g>, including audio, photos, payment info, passwords, and messages.<br/><br/><xliff:g id="app_name" example="Exo">%1$s</xliff:g> will be able to stream apps to <xliff:g id="device_name" example="Chromebook">%3$s</xliff:g> until you remove access to this permission.</string>
<!-- Description of the helper dialog for NEARBY_DEVICE_STREAMING profile. [CHAR LIMIT=NONE] -->
- <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="NearbyStreamerApp">%1$s</xliff:g> is requesting permission on behalf of your <xliff:g id="device_name" example="NearbyDevice">%2$s</xliff:g> to stream apps and other system features between your devices</string>
+ <string name="helper_summary_nearby_device_streaming"><xliff:g id="app_name" example="Exo">%1$s</xliff:g> is requesting permission on behalf of <xliff:g id="device_name" example="Chromebook">%2$s</xliff:g> to stream apps from your <xliff:g id="device_type" example="phone">%3$s</xliff:g></string>
<!-- ================= null profile ================= -->
diff --git a/packages/PackageInstaller/TEST_MAPPING b/packages/PackageInstaller/TEST_MAPPING
index 91882fd..50db501 100644
--- a/packages/PackageInstaller/TEST_MAPPING
+++ b/packages/PackageInstaller/TEST_MAPPING
@@ -1,6 +1,17 @@
{
"presubmit": [
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"options":[
{
@@ -12,6 +23,17 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"options":[
{
diff --git a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml b/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml
deleted file mode 100644
index a1761e5..0000000
--- a/packages/SettingsLib/ButtonPreference/res/values-v35/attrs_expressive.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2024 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<resources>
- <declare-styleable name="ButtonPreference">
- <attr name="buttonType" format="enum">
- <enum name="filled" value="0"/>
- <enum name="tonal" value="1"/>
- <enum name="outline" value="2"/>
- </attr>
- <attr name="buttonSize" format="enum">
- <enum name="normal" value="0"/>
- <enum name="large" value="1"/>
- <enum name="extra" value="2"/>
- </attr>
- </declare-styleable>
-</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ButtonPreference/res/values/attrs.xml b/packages/SettingsLib/ButtonPreference/res/values/attrs.xml
index 9c1e503f..970eeb2 100644
--- a/packages/SettingsLib/ButtonPreference/res/values/attrs.xml
+++ b/packages/SettingsLib/ButtonPreference/res/values/attrs.xml
@@ -18,12 +18,12 @@
<resources>
<declare-styleable name="ButtonPreference">
<attr name="android:gravity" />
- <attr name="buttonType" format="enum">
+ <attr name="buttonPreferenceType" format="enum">
<enum name="filled" value="0"/>
<enum name="tonal" value="1"/>
<enum name="outline" value="2"/>
</attr>
- <attr name="buttonSize" format="enum">
+ <attr name="buttonPreferenceSize" format="enum">
<enum name="normal" value="0"/>
<enum name="large" value="1"/>
<enum name="extra" value="2"/>
diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
index 0041eb2..979ff96 100644
--- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
+++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java
@@ -137,8 +137,8 @@
mGravity = a.getInt(R.styleable.ButtonPreference_android_gravity, Gravity.START);
if (SettingsThemeHelper.isExpressiveTheme(context)) {
- int type = a.getInt(R.styleable.ButtonPreference_buttonType, 0);
- int size = a.getInt(R.styleable.ButtonPreference_buttonSize, 0);
+ int type = a.getInt(R.styleable.ButtonPreference_buttonPreferenceType, 0);
+ int size = a.getInt(R.styleable.ButtonPreference_buttonPreferenceSize, 0);
resId = ButtonStyle.getLayoutId(type, size);
}
a.recycle();
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
index 34de5c4..e6726dc 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
@@ -68,6 +68,7 @@
private View mExtraWidgetContainer;
private ImageView mExtraWidget;
+ @Nullable private String mExtraWidgetContentDescription;
private boolean mIsCheckBox = false; // whether to display this button as a checkbox
private View.OnClickListener mExtraWidgetOnClickListener;
@@ -173,6 +174,12 @@
setExtraWidgetOnClickListener(mExtraWidgetOnClickListener);
+ if (mExtraWidget != null) {
+ mExtraWidget.setContentDescription(mExtraWidgetContentDescription != null
+ ? mExtraWidgetContentDescription
+ : getContext().getString(R.string.settings_label));
+ }
+
if (Flags.allowSetTitleMaxLines()) {
TextView title = (TextView) holder.findViewById(android.R.id.title);
title.setMaxLines(mTitleMaxLines);
@@ -210,6 +217,17 @@
}
/**
+ * Sets the content description of the extra widget. If {@code null}, a default content
+ * description will be used ("Settings").
+ */
+ public void setExtraWidgetContentDescription(@Nullable String contentDescription) {
+ if (!TextUtils.equals(mExtraWidgetContentDescription, contentDescription)) {
+ mExtraWidgetContentDescription = contentDescription;
+ notifyChanged();
+ }
+ }
+
+ /**
* Returns whether this preference is a checkbox.
*/
public boolean isCheckBox() {
diff --git a/packages/SettingsLib/res/values-en-rGB/strings.xml b/packages/SettingsLib/res/values-en-rGB/strings.xml
index d4e01de..2fd84f3 100644
--- a/packages/SettingsLib/res/values-en-rGB/strings.xml
+++ b/packages/SettingsLib/res/values-en-rGB/strings.xml
@@ -585,7 +585,6 @@
<string name="media_transfer_this_device_name_desktop" msgid="7912386128141470452">"This computer (internal)"</string>
<!-- no translation found for media_transfer_this_device_name_tv (5285685336836896535) -->
<skip />
- <string name="media_transfer_internal_mic" msgid="797333824290228595">"Microphone (internal)"</string>
<string name="media_transfer_dock_speaker_device_name" msgid="2856219597113881950">"Dock speaker"</string>
<string name="media_transfer_external_device_name" msgid="2588672258721846418">"External device"</string>
<string name="media_transfer_default_device_name" msgid="4315604017399871828">"Connected device"</string>
@@ -688,9 +687,11 @@
<string name="cached_apps_freezer_reboot_dialog_text" msgid="695330563489230096">"Your device must be rebooted for this change to apply. Reboot now or cancel."</string>
<string name="media_transfer_wired_headphone_name" msgid="8698668536022665254">"Wired headphones"</string>
<string name="media_transfer_headphone_name" msgid="1131962659136578852">"Headphone"</string>
- <string name="media_transfer_usb_speaker_name" msgid="4736537022543593896">"USB speaker"</string>
+ <!-- no translation found for media_transfer_usb_audio_name (1789292056757821355) -->
+ <skip />
<string name="media_transfer_wired_device_mic_name" msgid="7417067197803840965">"Mic jack"</string>
- <string name="media_transfer_usb_device_mic_name" msgid="9189914846215516322">"USB mic"</string>
+ <!-- no translation found for media_transfer_usb_device_mic_name (7171789543226269822) -->
+ <skip />
<string name="wifi_hotspot_switch_on_text" msgid="9212273118217786155">"On"</string>
<string name="wifi_hotspot_switch_off_text" msgid="7245567251496959764">"Off"</string>
<string name="carrier_network_change_mode" msgid="4257621815706644026">"Operator network changing"</string>
diff --git a/packages/SettingsLib/res/values-pt-rBR/strings.xml b/packages/SettingsLib/res/values-pt-rBR/strings.xml
index 7f5bb0f..e286643 100644
--- a/packages/SettingsLib/res/values-pt-rBR/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rBR/strings.xml
@@ -585,7 +585,6 @@
<string name="media_transfer_this_device_name_desktop" msgid="7912386128141470452">"Este computador (interno)"</string>
<!-- no translation found for media_transfer_this_device_name_tv (5285685336836896535) -->
<skip />
- <string name="media_transfer_internal_mic" msgid="797333824290228595">"Microfone (interno)"</string>
<string name="media_transfer_dock_speaker_device_name" msgid="2856219597113881950">"Alto-falante da base"</string>
<string name="media_transfer_external_device_name" msgid="2588672258721846418">"Dispositivo externo"</string>
<string name="media_transfer_default_device_name" msgid="4315604017399871828">"Dispositivo conectado"</string>
@@ -688,9 +687,11 @@
<string name="cached_apps_freezer_reboot_dialog_text" msgid="695330563489230096">"É necessário reinicializar o dispositivo para que a mudança seja aplicada. Faça isso agora ou cancele."</string>
<string name="media_transfer_wired_headphone_name" msgid="8698668536022665254">"Fones de ouvido com fio"</string>
<string name="media_transfer_headphone_name" msgid="1131962659136578852">"Fone de ouvido"</string>
- <string name="media_transfer_usb_speaker_name" msgid="4736537022543593896">"Alto-falante USB"</string>
+ <!-- no translation found for media_transfer_usb_audio_name (1789292056757821355) -->
+ <skip />
<string name="media_transfer_wired_device_mic_name" msgid="7417067197803840965">"Entrada para microfone"</string>
- <string name="media_transfer_usb_device_mic_name" msgid="9189914846215516322">"Microfone USB"</string>
+ <!-- no translation found for media_transfer_usb_device_mic_name (7171789543226269822) -->
+ <skip />
<string name="wifi_hotspot_switch_on_text" msgid="9212273118217786155">"Ativado"</string>
<string name="wifi_hotspot_switch_off_text" msgid="7245567251496959764">"Desativado"</string>
<string name="carrier_network_change_mode" msgid="4257621815706644026">"Alteração de rede da operadora"</string>
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java
index 243ce85..2b8b3b7 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java
@@ -19,8 +19,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
import android.app.Application;
import android.platform.test.annotations.DisableFlags;
@@ -68,7 +66,7 @@
mPreference = new SelectorWithWidgetPreference(mContext);
View view = LayoutInflater.from(mContext)
- .inflate(R.layout.preference_selector_with_widget, null /* root */);
+ .inflate(mPreference.getLayoutResource(), null /* root */);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
mPreference.onBindViewHolder(preferenceViewHolder);
@@ -104,28 +102,28 @@
@Test
public void onBindViewHolder_withSummary_containerShouldBeVisible() {
mPreference.setSummary("some summary");
- View summaryContainer = new View(mContext);
- View view = mock(View.class);
- when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null /* root */);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
mPreference.onBindViewHolder(preferenceViewHolder);
+ View summaryContainer = view.findViewById(R.id.summary_container);
assertEquals(View.VISIBLE, summaryContainer.getVisibility());
}
@Test
public void onBindViewHolder_emptySummary_containerShouldBeGone() {
mPreference.setSummary("");
- View summaryContainer = new View(mContext);
- View view = mock(View.class);
- when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null /* root */);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
mPreference.onBindViewHolder(preferenceViewHolder);
+ View summaryContainer = view.findViewById(R.id.summary_container);
assertEquals(View.GONE, summaryContainer.getVisibility());
}
@@ -184,25 +182,49 @@
}
@Test
- public void nullSummary_containerShouldBeGone() {
- mPreference.setSummary(null);
- View summaryContainer = new View(mContext);
- View view = mock(View.class);
- when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer);
+ public void onBindViewHolder_appliesWidgetContentDescription() {
+ mPreference = new SelectorWithWidgetPreference(mContext);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), /* root= */ null);
PreferenceViewHolder preferenceViewHolder =
PreferenceViewHolder.createInstanceForTests(view);
+
+ mPreference.setExtraWidgetContentDescription("this is clearer");
mPreference.onBindViewHolder(preferenceViewHolder);
+
+ View widget = preferenceViewHolder.findViewById(R.id.selector_extra_widget);
+ assertThat(widget.getContentDescription().toString()).isEqualTo("this is clearer");
+
+ mPreference.setExtraWidgetContentDescription(null);
+ mPreference.onBindViewHolder(preferenceViewHolder);
+
+ assertThat(widget.getContentDescription().toString()).isEqualTo("Settings");
+ }
+
+ @Test
+ public void nullSummary_containerShouldBeGone() {
+ mPreference.setSummary(null);
+ View view = LayoutInflater.from(mContext)
+ .inflate(mPreference.getLayoutResource(), null /* root */);
+ PreferenceViewHolder preferenceViewHolder =
+ PreferenceViewHolder.createInstanceForTests(view);
+
+ mPreference.onBindViewHolder(preferenceViewHolder);
+
+ View summaryContainer = view.findViewById(R.id.summary_container);
assertEquals(View.GONE, summaryContainer.getVisibility());
}
@Test
public void setAppendixVisibility_setGone_shouldBeGone() {
mPreference.setAppendixVisibility(View.GONE);
-
View view = LayoutInflater.from(mContext)
- .inflate(R.layout.preference_selector_with_widget, null /* root */);
- PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view);
+ .inflate(mPreference.getLayoutResource(), null /* root */);
+ PreferenceViewHolder holder =
+ PreferenceViewHolder.createInstanceForTests(view);
+
mPreference.onBindViewHolder(holder);
+
assertThat(holder.findViewById(R.id.appendix).getVisibility()).isEqualTo(View.GONE);
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 5e31da4..4dc8424 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -168,10 +168,6 @@
Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
Settings.Secure.ZEN_DURATION,
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION,
- Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION,
- Settings.Secure.ZEN_SETTINGS_UPDATED,
- Settings.Secure.ZEN_SETTINGS_SUGGESTION_VIEWED,
Settings.Secure.CHARGING_SOUNDS_ENABLED,
Settings.Secure.CHARGING_VIBRATION_ENABLED,
Settings.Secure.ACCESSIBILITY_NON_INTERACTIVE_UI_TIMEOUT_MS,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index f6e1057..0773bd7 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -186,7 +186,7 @@
VALIDATORS.put(
Global.STEM_PRIMARY_BUTTON_SHORT_PRESS, new InclusiveIntegerRangeValidator(0, 1));
VALIDATORS.put(
- Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 1));
+ Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS, new InclusiveIntegerRangeValidator(0, 2));
VALIDATORS.put(
Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS, new InclusiveIntegerRangeValidator(0, 1));
VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index b3f7374..688676d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -247,10 +247,6 @@
VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);
- VALIDATORS.put(Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.SHOW_ZEN_SETTINGS_SUGGESTION, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.ZEN_SETTINGS_UPDATED, BOOLEAN_VALIDATOR);
- VALIDATORS.put(Secure.ZEN_SETTINGS_SUGGESTION_VIEWED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.CHARGING_VIBRATION_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 3c24f5c..2034f36 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2734,18 +2734,6 @@
Settings.Secure.ZEN_DURATION,
SecureSettingsProto.Zen.DURATION);
dumpSetting(s, p,
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION,
- SecureSettingsProto.Zen.SHOW_ZEN_UPGRADE_NOTIFICATION);
- dumpSetting(s, p,
- Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION,
- SecureSettingsProto.Zen.SHOW_ZEN_SETTINGS_SUGGESTION);
- dumpSetting(s, p,
- Settings.Secure.ZEN_SETTINGS_UPDATED,
- SecureSettingsProto.Zen.SETTINGS_UPDATED);
- dumpSetting(s, p,
- Settings.Secure.ZEN_SETTINGS_SUGGESTION_VIEWED,
- SecureSettingsProto.Zen.SETTINGS_SUGGESTION_VIEWED);
- dumpSetting(s, p,
Settings.Secure.CHARGE_OPTIMIZATION_MODE,
SecureSettingsProto.CHARGE_OPTIMIZATION_MODE);
p.end(zenToken);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 749ad0a..a8af43f5 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -4771,9 +4771,9 @@
}
if (currentVersion == 169) {
- // Version 169: Set the default value for Secure Settings ZEN_DURATION,
- // SHOW_ZEN_SETTINGS_SUGGESTION, ZEN_SETTINGS_UPDATE and
- // ZEN_SETTINGS_SUGGESTION_VIEWED
+ // Version 169: Set the default value for Secure Settings ZEN_DURATION.
+ // Also used to update SHOW_ZEN_SETTINGS_SUGGESTION, ZEN_SETTINGS_UPDATE and
+ // ZEN_SETTINGS_SUGGESTION_VIEWED, but those properties are gone now.
final SettingsState globalSettings = getGlobalSettingsLocked();
final Setting globalZenDuration = globalSettings.getSettingLocked(
@@ -4801,33 +4801,6 @@
SettingsState.SYSTEM_PACKAGE_NAME);
}
- // SHOW_ZEN_SETTINGS_SUGGESTION
- final Setting currentShowZenSettingSuggestion = secureSettings.getSettingLocked(
- Secure.SHOW_ZEN_SETTINGS_SUGGESTION);
- if (currentShowZenSettingSuggestion.isNull()) {
- secureSettings.insertSettingOverrideableByRestoreLocked(
- Secure.SHOW_ZEN_SETTINGS_SUGGESTION, "1",
- null, true, SettingsState.SYSTEM_PACKAGE_NAME);
- }
-
- // ZEN_SETTINGS_UPDATED
- final Setting currentUpdatedSetting = secureSettings.getSettingLocked(
- Secure.ZEN_SETTINGS_UPDATED);
- if (currentUpdatedSetting.isNull()) {
- secureSettings.insertSettingOverrideableByRestoreLocked(
- Secure.ZEN_SETTINGS_UPDATED, "0",
- null, true, SettingsState.SYSTEM_PACKAGE_NAME);
- }
-
- // ZEN_SETTINGS_SUGGESTION_VIEWED
- final Setting currentSettingSuggestionViewed = secureSettings.getSettingLocked(
- Secure.ZEN_SETTINGS_SUGGESTION_VIEWED);
- if (currentSettingSuggestionViewed.isNull()) {
- secureSettings.insertSettingOverrideableByRestoreLocked(
- Secure.ZEN_SETTINGS_SUGGESTION_VIEWED, "0",
- null, true, SettingsState.SYSTEM_PACKAGE_NAME);
- }
-
currentVersion = 170;
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index b57629f..6c3ba68 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -630,6 +630,20 @@
}
flag {
+ name: "screenshot_multidisplay_focus_change"
+ namespace: "systemui"
+ description: "Only capture a single display when screenshotting"
+ bug: "362720389"
+}
+
+flag {
+ name: "screenshot_policy_split_and_desktop_mode"
+ namespace: "systemui"
+ description: "Improves screenshot policy handling for split screen and desktop mode."
+ bug: "365597999"
+}
+
+flag {
name: "run_fingerprint_detect_on_dismissible_keyguard"
namespace: "systemui"
description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
@@ -1067,6 +1081,16 @@
}
flag {
+ name: "dream_overlay_updated_font"
+ namespace: "systemui"
+ description: "Flag to enable updated font settings for dream overlay"
+ bug: "349656117"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "app_clips_backlinks"
namespace: "systemui"
description: "Enables Backlinks improvement feature in App Clips"
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index dc9e267..56de096 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -132,7 +132,6 @@
state = state,
modifier = modifier.fillMaxSize(),
swipeSourceDetector = viewModel.edgeDetector,
- gestureFilter = viewModel::shouldFilterGesture,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index ac58ab5..9d5252e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -1,5 +1,6 @@
package com.android.systemui.scene.ui.composable
+import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import com.android.compose.animation.scene.ProgressConverter
import com.android.compose.animation.scene.TransitionKey
@@ -44,6 +45,7 @@
// Overscroll progress starts linearly with some resistance (3f) and slowly approaches 0.2f
defaultOverscrollProgressConverter = ProgressConverter.tanh(maxProgress = 0.2f, tilt = 3f)
+ defaultSwipeSpec = spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f)
// Scene transitions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 5e6f88e..5fa5db8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -124,10 +124,6 @@
overSlop: Float,
pointersDown: Int,
): DragController {
- if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
- return NoOpDragController
- }
-
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
@@ -189,7 +185,7 @@
return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation)
}
- private fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
+ internal fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? {
if (startedPosition == null) return null
return layoutImpl.swipeSourceDetector.source(
layoutSize = layoutImpl.lastSize,
@@ -199,7 +195,7 @@
)
}
- private fun resolveSwipe(
+ internal fun resolveSwipe(
pointersDown: Int,
fromSource: SwipeSource.Resolved?,
isUpOrLeft: Boolean,
@@ -559,6 +555,14 @@
val connection: PriorityNestedScrollConnection = nestedScrollConnection()
+ private fun PointersInfo.resolveSwipe(isUpOrLeft: Boolean): Swipe.Resolved {
+ return draggableHandler.resolveSwipe(
+ pointersDown = pointersDown,
+ fromSource = draggableHandler.resolveSwipeSource(startedPosition),
+ isUpOrLeft = isUpOrLeft,
+ )
+ }
+
private fun nestedScrollConnection(): PriorityNestedScrollConnection {
// If we performed a long gesture before entering priority mode, we would have to avoid
// moving on to the next scene.
@@ -575,36 +579,19 @@
val transitionState = layoutState.transitionState
val scene = transitionState.currentScene
val fromScene = layoutImpl.scene(scene)
- val nextScene =
+ val resolvedSwipe =
when {
- amount < 0f -> {
- val actionUpOrLeft =
- Swipe.Resolved(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Resolved.Left
- Orientation.Vertical -> SwipeDirection.Resolved.Up
- },
- pointerCount = pointersInfo().pointersDown,
- fromSource = null,
- )
- fromScene.userActions[actionUpOrLeft]
- }
- amount > 0f -> {
- val actionDownOrRight =
- Swipe.Resolved(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Resolved.Right
- Orientation.Vertical -> SwipeDirection.Resolved.Down
- },
- pointerCount = pointersInfo().pointersDown,
- fromSource = null,
- )
- fromScene.userActions[actionDownOrRight]
- }
+ amount < 0f -> pointersInfo().resolveSwipe(isUpOrLeft = true)
+ amount > 0f -> pointersInfo().resolveSwipe(isUpOrLeft = false)
else -> null
}
+ val nextScene =
+ resolvedSwipe?.let {
+ fromScene.userActions[it]
+ ?: if (it.fromSource != null) {
+ fromScene.userActions[it.copy(fromSource = null)]
+ } else null
+ }
if (nextScene != null) return true
if (transitionState !is TransitionState.Idle) return false
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 4ae9718..dc3135d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -213,8 +213,9 @@
internal fun pointersInfo(): PointersInfo {
return PointersInfo(
+ // This may be null, i.e. when the user uses TalkBack
startedPosition = startedPosition,
- // Note: We could have 0 pointers during fling or for other reasons.
+ // We could have 0 pointers during fling or for other reasons.
pointersDown = pointersDown.coerceAtLeast(1),
)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 6e89814..cec8883 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -47,9 +47,6 @@
* @param state the state of this layout.
* @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
* if any.
- * @param gestureFilter decides whether a drag gesture that started at the given start position
- * should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
- * returns `false`, the drag gesture will be handled.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param builder the configuration of the different scenes and overlays of this layout.
@@ -60,7 +57,6 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
- gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -69,7 +65,6 @@
modifier,
swipeSourceDetector,
swipeDetector,
- gestureFilter,
transitionInterceptionThreshold,
onLayoutImpl = null,
builder,
@@ -621,7 +616,6 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
- gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
@@ -638,7 +632,6 @@
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = builder,
animationScope = animationScope,
- gestureFilter = gestureFilter,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 9e7be37..65c4043 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,7 +31,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
@@ -71,7 +70,6 @@
* animations.
*/
internal val animationScope: CoroutineScope,
- internal val gestureFilter: (startedPosition: Offset) -> Boolean,
) {
/**
* The map of [Scene]s.
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 b358faf..879dc54 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
@@ -152,6 +152,7 @@
internal val DefaultSwipeSpec =
spring(
stiffness = Spring.StiffnessMediumLow,
+ dampingRatio = Spring.DampingRatioLowBouncy,
visibilityThreshold = OffsetVisibilityThreshold,
)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index f758102..54ee783 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,7 +17,6 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Stable
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -32,8 +31,6 @@
val DefaultSwipeDetector = PassthroughSwipeDetector()
-val DefaultGestureFilter = { _: Offset -> false }
-
/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
class PassthroughSwipeDetector : SwipeDetector {
override fun detectSwipe(change: PointerInputChange): Boolean {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index a6ebb0e..a3641e6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -34,71 +34,76 @@
* Note: Call [reset] before destroying this object to make sure you always get a call to [onStop]
* after [onStart].
*
- * @sample com.android.compose.animation.scene.rememberSwipeToSceneNestedScrollConnection
+ * @sample LargeTopAppBarNestedScrollConnection
+ * @sample com.android.compose.animation.scene.NestedScrollHandlerImpl.nestedScrollConnection
*/
class PriorityNestedScrollConnection(
- private val canStartPreScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
- private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
- private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean,
+ orientation: Orientation,
+ private val canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
+ private val canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
+ private val canStartPostFling: (velocityAvailable: Float) -> Boolean,
private val canContinueScroll: (source: NestedScrollSource) -> Boolean,
private val canScrollOnFling: Boolean,
- private val onStart: (offsetAvailable: Offset) -> Unit,
- private val onScroll: (offsetAvailable: Offset) -> Offset,
- private val onStop: (velocityAvailable: Velocity) -> SuspendedValue<Velocity>,
-) : NestedScrollConnection {
+ private val onStart: (offsetAvailable: Float) -> Unit,
+ private val onScroll: (offsetAvailable: Float) -> Float,
+ private val onStop: (velocityAvailable: Float) -> SuspendedValue<Float>,
+) : NestedScrollConnection, SpaceVectorConverter by SpaceVectorConverter(orientation) {
/** In priority mode [onPreScroll] events are first consumed by the parent, via [onScroll]. */
private var isPriorityMode = false
- private var offsetScrolledBeforePriorityMode = Offset.Zero
+ private var offsetScrolledBeforePriorityMode = 0f
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
+ val availableFloat = available.toFloat()
// The offset before the start takes into account the up and down movements, starting from
// the beginning or from the last fling gesture.
- val offsetBeforeStart = offsetScrolledBeforePriorityMode - available
+ val offsetBeforeStart = offsetScrolledBeforePriorityMode - availableFloat
if (
isPriorityMode ||
(source == NestedScrollSource.SideEffect && !canScrollOnFling) ||
- !canStartPostScroll(available, offsetBeforeStart)
+ !canStartPostScroll(availableFloat, offsetBeforeStart)
) {
// The priority mode cannot start so we won't consume the available offset.
return Offset.Zero
}
- return onPriorityStart(available)
+ return onPriorityStart(availableFloat).toOffset()
}
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
if (!isPriorityMode) {
if (source == NestedScrollSource.UserInput || canScrollOnFling) {
- if (canStartPreScroll(available, offsetScrolledBeforePriorityMode)) {
- return onPriorityStart(available)
+ val availableFloat = available.toFloat()
+ if (canStartPreScroll(availableFloat, offsetScrolledBeforePriorityMode)) {
+ return onPriorityStart(availableFloat).toOffset()
}
// We want to track the amount of offset consumed before entering priority mode
- offsetScrolledBeforePriorityMode += available
+ offsetScrolledBeforePriorityMode += availableFloat
}
return Offset.Zero
}
+ val availableFloat = available.toFloat()
if (!canContinueScroll(source)) {
// Step 3a: We have lost priority and we no longer need to intercept scroll events.
- onPriorityStop(velocity = Velocity.Zero)
+ onPriorityStop(velocity = 0f)
- // We've just reset offsetScrolledBeforePriorityMode to Offset.Zero
+ // We've just reset offsetScrolledBeforePriorityMode to 0f
// We want to track the amount of offset consumed before entering priority mode
- offsetScrolledBeforePriorityMode += available
+ offsetScrolledBeforePriorityMode += availableFloat
return Offset.Zero
}
// Step 2: We have the priority and can consume the scroll events.
- return onScroll(available)
+ return onScroll(availableFloat).toOffset()
}
override suspend fun onPreFling(available: Velocity): Velocity {
@@ -108,15 +113,16 @@
}
// Step 3b: The finger is lifted, we can stop intercepting scroll events and use the speed
// of the fling gesture.
- return onPriorityStop(velocity = available).invoke()
+ return onPriorityStop(velocity = available.toFloat()).invoke().toVelocity()
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ val availableFloat = available.toFloat()
if (isPriorityMode) {
- return onPriorityStop(velocity = available).invoke()
+ return onPriorityStop(velocity = availableFloat).invoke().toVelocity()
}
- if (!canStartPostFling(available)) {
+ if (!canStartPostFling(availableFloat)) {
return Velocity.Zero
}
@@ -124,11 +130,11 @@
// given the available velocity.
// TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the
// overscroll behavior on the Scene level.
- val smallOffset = Offset(available.x.sign, available.y.sign)
- onPriorityStart(available = smallOffset)
+ val smallOffset = availableFloat.sign
+ onPriorityStart(availableOffset = smallOffset)
// This is the last event of a scroll gesture.
- return onPriorityStop(available).invoke()
+ return onPriorityStop(availableFloat).invoke().toVelocity()
}
/**
@@ -138,10 +144,10 @@
*/
fun reset() {
// Step 3c: To ensure that an onStop is always called for every onStart.
- onPriorityStop(velocity = Velocity.Zero)
+ onPriorityStop(velocity = 0f)
}
- private fun onPriorityStart(available: Offset): Offset {
+ private fun onPriorityStart(availableOffset: Float): Float {
if (isPriorityMode) {
error("This should never happen, onPriorityStart() was called when isPriorityMode")
}
@@ -152,17 +158,17 @@
// Note: onStop will be called if we cannot continue to scroll (step 3a), or the finger is
// lifted (step 3b), or this object has been destroyed (step 3c).
- onStart(available)
+ onStart(availableOffset)
- return onScroll(available)
+ return onScroll(availableOffset)
}
- private fun onPriorityStop(velocity: Velocity): SuspendedValue<Velocity> {
+ private fun onPriorityStop(velocity: Float): SuspendedValue<Float> {
// We can restart tracking the consumed offsets from scratch.
- offsetScrolledBeforePriorityMode = Offset.Zero
+ offsetScrolledBeforePriorityMode = 0f
if (!isPriorityMode) {
- return { Velocity.Zero }
+ return { 0f }
}
isPriorityMode = false
@@ -170,38 +176,3 @@
return onStop(velocity)
}
}
-
-fun PriorityNestedScrollConnection(
- orientation: Orientation,
- canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
- canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
- canStartPostFling: (velocityAvailable: Float) -> Boolean,
- canContinueScroll: (source: NestedScrollSource) -> Boolean,
- canScrollOnFling: Boolean,
- onStart: (offsetAvailable: Float) -> Unit,
- onScroll: (offsetAvailable: Float) -> Float,
- onStop: (velocityAvailable: Float) -> SuspendedValue<Float>,
-) =
- with(SpaceVectorConverter(orientation)) {
- PriorityNestedScrollConnection(
- canStartPreScroll = { offsetAvailable: Offset, offsetBeforeStart: Offset ->
- canStartPreScroll(offsetAvailable.toFloat(), offsetBeforeStart.toFloat())
- },
- canStartPostScroll = { offsetAvailable: Offset, offsetBeforeStart: Offset ->
- canStartPostScroll(offsetAvailable.toFloat(), offsetBeforeStart.toFloat())
- },
- canStartPostFling = { velocityAvailable: Velocity ->
- canStartPostFling(velocityAvailable.toFloat())
- },
- canContinueScroll = canContinueScroll,
- canScrollOnFling = canScrollOnFling,
- onStart = { offsetAvailable -> onStart(offsetAvailable.toFloat()) },
- onScroll = { offsetAvailable: Offset ->
- onScroll(offsetAvailable.toFloat()).toOffset()
- },
- onStop = { velocityAvailable: Velocity ->
- val consumedVelocity = onStop(velocityAvailable.toFloat())
- suspend { consumedVelocity.invoke().toVelocity() }
- },
- )
- }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 2c41b35..302f207 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -109,8 +109,6 @@
val transitionInterceptionThreshold = 0.05f
- var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
-
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
@@ -123,13 +121,16 @@
// Use testScope and not backgroundScope here because backgroundScope does not
// work well with advanceUntilIdle(), which is used by some tests.
animationScope = testScope,
- gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical)
val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal)
+ var pointerInfoOwner: () -> PointersInfo = {
+ PointersInfo(startedPosition = Offset.Zero, pointersDown = 1)
+ }
+
fun nestedScrollConnection(
nestedScrollBehavior: NestedScrollBehavior,
isExternalOverscrollGesture: Boolean = false,
@@ -140,9 +141,7 @@
topOrLeftBehavior = nestedScrollBehavior,
bottomOrRightBehavior = nestedScrollBehavior,
isExternalOverscrollGesture = { isExternalOverscrollGesture },
- pointersInfoOwner = {
- PointersInfo(startedPosition = Offset.Zero, pointersDown = 1)
- },
+ pointersInfoOwner = { pointerInfoOwner() },
)
.connection
@@ -156,11 +155,18 @@
fun downOffset(fractionOfScreen: Float) =
if (fractionOfScreen < 0f) {
- error("upOffset() is required, not implemented yet")
+ error("use upOffset()")
} else {
Offset(x = 0f, y = down(fractionOfScreen))
}
+ fun upOffset(fractionOfScreen: Float) =
+ if (fractionOfScreen < 0f) {
+ error("use downOffset()")
+ } else {
+ Offset(x = 0f, y = up(fractionOfScreen))
+ }
+
val transitionState: TransitionState
get() = layoutState.transitionState
@@ -343,13 +349,6 @@
}
@Test
- fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
- gestureFilter = { _ -> true }
- onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
- assertIdle(currentScene = SceneA)
- }
-
- @Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
assertTransition(currentScene = SceneA)
@@ -1135,6 +1134,45 @@
}
@Test
+ fun nestedScrollUseFromSourceInfo() = runGestureTest {
+ // Start at scene C.
+ navigateToSceneC()
+ val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
+
+ // Drag from the **top** of the screen
+ pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1) }
+ assertIdle(currentScene = SceneC)
+
+ nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ // userAction: Swipe.Up to SceneB
+ toScene = SceneB,
+ progress = 0.1f,
+ )
+
+ // Reset to SceneC
+ nestedScroll.preFling(Velocity.Zero)
+ advanceUntilIdle()
+
+ // Drag from the **bottom** of the screen
+ pointerInfoOwner = {
+ PointersInfo(startedPosition = Offset(0f, SCREEN_SIZE), pointersDown = 1)
+ }
+ assertIdle(currentScene = SceneC)
+
+ nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ // userAction: Swipe(SwipeDirection.Up, fromSource = Edge.Bottom) to SceneA
+ toScene = SceneA,
+ progress = 0.1f,
+ )
+ }
+
+ @Test
fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
// Swipe up from the middle to transition to scene B.
val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
@@ -1203,7 +1241,8 @@
fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
// Make scene B overscrollable.
layoutState.transitions = transitions {
- from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
+ defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
+ from(SceneA, to = SceneB) {}
overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) }
}
@@ -1234,7 +1273,8 @@
fun overscroll_releaseBetween0And100Percent_down() = runGestureTest {
// Make scene C overscrollable.
layoutState.transitions = transitions {
- from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
+ defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)
+ from(SceneA, to = SceneC) {}
overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt
index bde7699..badc43b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt
@@ -18,8 +18,9 @@
package com.android.compose.nestedscroll
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
import androidx.compose.ui.unit.Velocity
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
@@ -35,13 +36,14 @@
private var canStartPostFling = false
private var canContinueScroll = false
private var isStarted = false
- private var lastScroll: Offset? = null
- private var returnOnScroll = Offset.Zero
- private var lastStop: Velocity? = null
- private var returnOnStop = Velocity.Zero
+ private var lastScroll: Float? = null
+ private var returnOnScroll = 0f
+ private var lastStop: Float? = null
+ private var returnOnStop = 0f
private val scrollConnection =
PriorityNestedScrollConnection(
+ orientation = Orientation.Vertical,
canStartPreScroll = { _, _ -> canStartPreScroll },
canStartPostScroll = { _, _ -> canStartPostScroll },
canStartPostFling = { canStartPostFling },
@@ -58,11 +60,6 @@
},
)
- private val offset1 = Offset(1f, 1f)
- private val offset2 = Offset(2f, 2f)
- private val velocity1 = Velocity(1f, 1f)
- private val velocity2 = Velocity(2f, 2f)
-
@Test
fun step1_priorityModeShouldStartOnlyOnPreScroll() = runTest {
canStartPreScroll = true
@@ -70,7 +67,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset.Zero,
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertThat(isStarted).isEqualTo(false)
@@ -80,7 +77,7 @@
scrollConnection.onPostFling(consumed = Velocity.Zero, available = Velocity.Zero)
assertThat(isStarted).isEqualTo(false)
- scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
+ scrollConnection.onPreScroll(available = Offset.Zero, source = UserInput)
assertThat(isStarted).isEqualTo(true)
}
@@ -89,7 +86,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset.Zero,
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
}
@@ -97,7 +94,7 @@
fun step1_priorityModeShouldStartOnlyOnPostScroll() = runTest {
canStartPostScroll = true
- scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
+ scrollConnection.onPreScroll(available = Offset.Zero, source = UserInput)
assertThat(isStarted).isEqualTo(false)
scrollConnection.onPreFling(available = Velocity.Zero)
@@ -115,7 +112,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset.Zero,
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertThat(isStarted).isEqualTo(false)
@@ -128,12 +125,12 @@
canStartPostScroll = true
scrollConnection.onPostScroll(
- consumed = offset1,
- available = offset2,
- source = NestedScrollSource.Drag,
+ consumed = Offset(1f, 1f),
+ available = Offset(2f, 2f),
+ source = UserInput,
)
- assertThat(lastScroll).isEqualTo(offset2)
+ assertThat(lastScroll).isEqualTo(2f)
}
@Test
@@ -141,13 +138,13 @@
startPriorityModePostScroll()
canContinueScroll = true
- scrollConnection.onPreScroll(available = offset1, source = NestedScrollSource.Drag)
- assertThat(lastScroll).isEqualTo(offset1)
+ scrollConnection.onPreScroll(available = Offset(1f, 1f), source = UserInput)
+ assertThat(lastScroll).isEqualTo(1f)
canContinueScroll = false
- scrollConnection.onPreScroll(available = offset2, source = NestedScrollSource.Drag)
- assertThat(lastScroll).isNotEqualTo(offset2)
- assertThat(lastScroll).isEqualTo(offset1)
+ scrollConnection.onPreScroll(available = Offset(2f, 2f), source = UserInput)
+ assertThat(lastScroll).isNotEqualTo(2f)
+ assertThat(lastScroll).isEqualTo(1f)
}
@Test
@@ -155,7 +152,7 @@
startPriorityModePostScroll()
canContinueScroll = false
- scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
+ scrollConnection.onPreScroll(available = Offset.Zero, source = UserInput)
assertThat(lastStop).isNotNull()
}
@@ -184,22 +181,22 @@
fun receive_onPostFling() = runTest {
canStartPostFling = true
- scrollConnection.onPostFling(consumed = velocity1, available = velocity2)
+ scrollConnection.onPostFling(consumed = Velocity(1f, 1f), available = Velocity(2f, 2f))
- assertThat(lastStop).isEqualTo(velocity2)
+ assertThat(lastStop).isEqualTo(2f)
}
@Test
fun step1_priorityModeShouldStartOnlyOnPostFling() = runTest {
canStartPostFling = true
- scrollConnection.onPreScroll(available = Offset.Zero, source = NestedScrollSource.Drag)
+ scrollConnection.onPreScroll(available = Offset.Zero, source = UserInput)
assertThat(isStarted).isEqualTo(false)
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset.Zero,
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertThat(isStarted).isEqualTo(false)
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index c399abc..81d92fa 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -36,6 +36,7 @@
"SystemUIPluginLib",
"SystemUIUnfoldLib",
"kotlinx_coroutines",
+ "monet",
"dagger2",
"jsr330",
],
diff --git a/packages/SystemUI/customization/res/values/ids.xml b/packages/SystemUI/customization/res/values/ids.xml
index 5eafbfc..ec466f0 100644
--- a/packages/SystemUI/customization/res/values/ids.xml
+++ b/packages/SystemUI/customization/res/values/ids.xml
@@ -6,4 +6,13 @@
<item type="id" name="weather_clock_weather_icon" />
<item type="id" name="weather_clock_temperature" />
<item type="id" name="weather_clock_alarm_dnd" />
+
+ <item type="id" name="HOUR_DIGIT_PAIR"/>
+ <item type="id" name="MINUTE_DIGIT_PAIR"/>
+ <item type="id" name="HOUR_FIRST_DIGIT"/>
+ <item type="id" name="HOUR_SECOND_DIGIT"/>
+ <item type="id" name="MINUTE_FIRST_DIGIT"/>
+ <item type="id" name="MINUTE_SECOND_DIGIT"/>
+ <item type="id" name="TIME_FULL_FORMAT"/>
+ <item type="id" name="DATE_FORMAT"/>
</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 1863cd8..9877406 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -62,6 +62,7 @@
// implement the get method and ensure a value is returned before initialization is complete.
private var logger = DEFAULT_LOGGER
get() = field ?: DEFAULT_LOGGER
+
var messageBuffer: MessageBuffer
get() = logger.buffer
set(value) {
@@ -123,24 +124,24 @@
attrs,
R.styleable.AnimatableClockView,
defStyleAttr,
- defStyleRes
+ defStyleRes,
)
try {
dozingWeightInternal =
animatableClockViewAttributes.getInt(
R.styleable.AnimatableClockView_dozeWeight,
- /* default = */ 100
+ /* default = */ 100,
)
lockScreenWeightInternal =
animatableClockViewAttributes.getInt(
R.styleable.AnimatableClockView_lockScreenWeight,
- /* default = */ 300
+ /* default = */ 300,
)
chargeAnimationDelay =
animatableClockViewAttributes.getInt(
R.styleable.AnimatableClockView_chargeAnimationDelay,
- /* default = */ 200
+ /* default = */ 200,
)
} finally {
animatableClockViewAttributes.recycle()
@@ -151,14 +152,14 @@
attrs,
android.R.styleable.TextView,
defStyleAttr,
- defStyleRes
+ defStyleRes,
)
try {
isSingleLineInternal =
textViewAttributes.getBoolean(
android.R.styleable.TextView_singleLine,
- /* default = */ false
+ /* default = */ false,
)
} finally {
textViewAttributes.recycle()
@@ -280,7 +281,7 @@
text: CharSequence,
start: Int,
lengthBefore: Int,
- lengthAfter: Int
+ lengthAfter: Int,
) {
logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
super.onTextChanged(text, start, lengthBefore, lengthAfter)
@@ -305,7 +306,7 @@
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = lockScreenWeight,
@@ -314,7 +315,7 @@
interpolator = null,
duration = COLOR_ANIM_DURATION,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -327,7 +328,7 @@
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = lockScreenWeight,
@@ -336,7 +337,7 @@
duration = APPEAR_ANIM_DURATION,
interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -353,7 +354,7 @@
interpolator = null,
duration = 0,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
setTextStyle(
weight = dozingWeightInternal,
@@ -362,7 +363,7 @@
interpolator = Interpolators.EMPHASIZED_DECELERATE,
duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -381,7 +382,7 @@
interpolator = null,
duration = CHARGE_ANIM_DURATION_PHASE_1,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
setTextStyle(
@@ -391,7 +392,7 @@
interpolator = null,
duration = CHARGE_ANIM_DURATION_PHASE_0,
delay = chargeAnimationDelay.toLong(),
- onAnimationEnd = startAnimPhase2
+ onAnimationEnd = startAnimPhase2,
)
}
@@ -404,7 +405,7 @@
interpolator = null,
duration = DOZE_ANIM_DURATION,
delay = 0,
- onAnimationEnd = null
+ onAnimationEnd = null,
)
}
@@ -444,7 +445,7 @@
interpolator: TimeInterpolator?,
duration: Long,
delay: Long,
- onAnimationEnd: Runnable?
+ onAnimationEnd: Runnable?,
) {
textAnimator?.let {
it.setTextStyle(
@@ -454,7 +455,7 @@
duration = duration,
interpolator = interpolator,
delay = delay,
- onAnimationEnd = onAnimationEnd
+ onAnimationEnd = onAnimationEnd,
)
it.glyphFilter = glyphFilter
}
@@ -468,7 +469,7 @@
duration = duration,
interpolator = interpolator,
delay = delay,
- onAnimationEnd = onAnimationEnd
+ onAnimationEnd = onAnimationEnd,
)
textAnimator.glyphFilter = glyphFilter
}
@@ -476,6 +477,7 @@
}
fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+
fun refreshFormat(use24HourFormat: Boolean) {
Patterns.update(context)
@@ -560,18 +562,11 @@
* @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
* it finished moving.
*/
- fun offsetGlyphsForStepClockAnimation(
- distance: Float,
- fraction: Float,
- ) {
+ fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
for (i in 0 until NUM_DIGITS) {
val dir = if (isLayoutRtl) -1 else 1
val digitFraction =
- getDigitFraction(
- digit = i,
- isMovingToCenter = distance > 0,
- fraction = fraction,
- )
+ getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
val moveAmountForDigit = dir * distance * digitFraction
glyphOffsets[i] = moveAmountForDigit
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
new file mode 100644
index 0000000..d001ef96
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
@@ -0,0 +1,448 @@
+/*
+ * 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.content.res.ColorStateList
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.util.TypedValue
+import com.android.internal.graphics.ColorUtils
+import com.android.internal.graphics.cam.Cam
+import com.android.internal.graphics.cam.CamUtils
+import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.monet.TonalPalette
+import java.io.IOException
+import kotlin.math.abs
+
+class AssetLoader
+private constructor(
+ private val pluginCtx: Context,
+ private val sysuiCtx: Context,
+ private val baseDir: String,
+ var colorScheme: ColorScheme?,
+ var seedColor: Int?,
+ var overrideChroma: Float?,
+ val typefaceCache: TypefaceCache,
+ val getThemeSeedColor: (Context) -> Int,
+ messageBuffer: MessageBuffer,
+) {
+ val logger = Logger(messageBuffer, TAG)
+ private val resources =
+ listOf(
+ Pair(pluginCtx.resources, pluginCtx.packageName),
+ Pair(sysuiCtx.resources, sysuiCtx.packageName),
+ )
+
+ constructor(
+ pluginCtx: Context,
+ sysuiCtx: Context,
+ baseDir: String,
+ messageBuffer: MessageBuffer,
+ getThemeSeedColor: ((Context) -> Int)? = null,
+ ) : this(
+ pluginCtx,
+ sysuiCtx,
+ baseDir,
+ colorScheme = null,
+ seedColor = null,
+ overrideChroma = null,
+ typefaceCache =
+ TypefaceCache(messageBuffer) { Typeface.createFromAsset(pluginCtx.assets, it) },
+ getThemeSeedColor = getThemeSeedColor ?: Companion::getThemeSeedColor,
+ messageBuffer = messageBuffer,
+ )
+
+ fun listAssets(path: String): List<String> {
+ return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList()
+ }
+
+ fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString)
+
+ fun readString(resStr: String): String {
+ val resPair = resolveResourceId(resStr)
+ if (resPair == null) {
+ throw IOException("Failed to parse string: $resStr")
+ }
+
+ val (res, id) = resPair
+ return res.getString(id)
+ }
+
+ fun tryReadColor(resStr: String): Int? = tryRead(resStr, ::readColor)
+
+ fun readColor(resStr: String): Int {
+ if (resStr.startsWith("#")) {
+ return Color.parseColor(resStr)
+ }
+
+ val schemeColor = tryParseColorFromScheme(resStr)
+ if (schemeColor != null) {
+ logColor("ColorScheme: $resStr", schemeColor)
+ return checkChroma(schemeColor)
+ }
+
+ val result = resolveColorResourceId(resStr)
+ if (result == null) {
+ throw IOException("Failed to parse color: $resStr")
+ }
+
+ val (res, colorId, targetTone) = result
+ val color = res.getColor(colorId)
+ if (targetTone == null || TonalPalette.SHADE_KEYS.contains(targetTone.toInt())) {
+ logColor("Resources: $resStr", color)
+ return checkChroma(color)
+ } else {
+ val interpolatedColor =
+ ColorStateList.valueOf(color)
+ .withLStar((1000f - targetTone) / 10f)
+ .getDefaultColor()
+ logColor("Resources (interpolated tone): $resStr", interpolatedColor)
+ return checkChroma(interpolatedColor)
+ }
+ }
+
+ private fun checkChroma(color: Int): Int {
+ return overrideChroma?.let {
+ val cam = Cam.fromInt(color)
+ val tone = CamUtils.lstarFromInt(color)
+ val result = ColorUtils.CAMToColor(cam.hue, it, tone)
+ logColor("Chroma override", result)
+ result
+ } ?: color
+ }
+
+ private fun tryParseColorFromScheme(resStr: String): Int? {
+ val colorScheme = this.colorScheme
+ if (colorScheme == null) {
+ logger.w("No color scheme available")
+ return null
+ }
+
+ val (packageName, category, name) = parseResourceId(resStr)
+ if (packageName != "android" || category != "color") {
+ logger.w("Failed to parse package from $resStr")
+ return null
+ }
+
+ var parts = name.split('_')
+ if (parts.size != 3) {
+ logger.w("Failed to find palette and shade from $name")
+ return null
+ }
+ val (_, paletteKey, shadeKeyStr) = parts
+
+ val palette =
+ when (paletteKey) {
+ "accent1" -> colorScheme.accent1
+ "accent2" -> colorScheme.accent2
+ "accent3" -> colorScheme.accent3
+ "neutral1" -> colorScheme.neutral1
+ "neutral2" -> colorScheme.neutral2
+ else -> return null
+ }
+
+ if (shadeKeyStr.contains("+") || shadeKeyStr.contains("-")) {
+ val signIndex = shadeKeyStr.indexOfLast { it == '-' || it == '+' }
+ // Use the tone of the seed color if it was set explicitly.
+ var baseTone =
+ if (seedColor != null) colorScheme.seedTone.toFloat()
+ else shadeKeyStr.substring(0, signIndex).toFloatOrNull()
+ val diff = shadeKeyStr.substring(signIndex).toFloatOrNull()
+
+ if (baseTone == null) {
+ logger.w("Failed to parse base tone from $shadeKeyStr")
+ return null
+ }
+
+ if (diff == null) {
+ logger.w("Failed to parse relative tone from $shadeKeyStr")
+ return null
+ }
+ return palette.getAtTone(baseTone + diff)
+ } else {
+ val shadeKey = shadeKeyStr.toIntOrNull()
+ if (shadeKey == null) {
+ logger.w("Failed to parse tone from $shadeKeyStr")
+ return null
+ }
+ return palette.allShadesMapped.get(shadeKey) ?: palette.getAtTone(shadeKey.toFloat())
+ }
+ }
+
+ fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr)
+
+ fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset)
+
+ fun readTextAsset(path: String): String {
+ return pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
+ val buffer = ByteArray(stream.available())
+ stream.read(buffer)
+ String(buffer)
+ }
+ }
+
+ fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset)
+
+ fun readDrawableAsset(path: String): Drawable {
+ var result: Drawable?
+
+ if (path.startsWith("@")) {
+ val pair = resolveResourceId(path)
+ if (pair == null) {
+ throw IOException("Failed to parse $path to an id")
+ }
+ val (res, id) = pair
+ result = res.getDrawable(id)
+ } else if (path.endsWith("xml")) {
+ // TODO(b/248609434): Support xml files in assets
+ throw IOException("Cannot load xml files from assets")
+ } else {
+ // Attempt to load as if it's a bitmap and directly loadable
+ result =
+ pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
+ Drawable.createFromResourceStream(
+ pluginCtx.resources,
+ TypedValue(),
+ stream,
+ null,
+ )
+ }
+ }
+
+ return result ?: throw IOException("Failed to load: $baseDir$path")
+ }
+
+ fun parseResourceId(resStr: String): Triple<String?, String, String> {
+ if (!resStr.startsWith("@")) {
+ throw IOException("Invalid resource id: $resStr; Must start with '@'")
+ }
+
+ // Parse out resource string
+ val parts = resStr.drop(1).split('/', ':')
+ return when (parts.size) {
+ 2 -> Triple(null, parts[0], parts[1])
+ 3 -> Triple(parts[0], parts[1], parts[2])
+ else -> throw IOException("Failed to parse resource string: $resStr")
+ }
+ }
+
+ fun resolveColorResourceId(resStr: String): Triple<Resources, Int, Float?>? {
+ var (packageName, category, name) = parseResourceId(resStr)
+
+ // Convert relative tonal specifiers to standard
+ val relIndex = name.indexOfLast { it == '_' }
+ val isToneRelative = name.contains("-") || name.contains("+")
+ val targetTone =
+ if (packageName != "android") {
+ null
+ } else if (isToneRelative) {
+ val signIndex = name.indexOfLast { it == '-' || it == '+' }
+ val baseTone = name.substring(relIndex + 1, signIndex).toFloatOrNull()
+ var diff = name.substring(signIndex).toFloatOrNull()
+ if (baseTone == null || diff == null) {
+ logger.w("Failed to parse relative tone from $name")
+ return null
+ }
+ baseTone + diff
+ } else {
+ val absTone = name.substring(relIndex + 1).toFloatOrNull()
+ if (absTone == null) {
+ logger.w("Failed to parse absolute tone from $name")
+ return null
+ }
+ absTone
+ }
+
+ if (
+ targetTone != null &&
+ (isToneRelative || !TonalPalette.SHADE_KEYS.contains(targetTone.toInt()))
+ ) {
+ val closeTone = TonalPalette.SHADE_KEYS.minBy { abs(it - targetTone) }
+ val prevName = name
+ name = name.substring(0, relIndex + 1) + closeTone
+ logger.i("Converted $prevName to $name")
+ }
+
+ val result = resolveResourceId(packageName, category, name)
+ if (result == null) {
+ return null
+ }
+
+ val (res, resId) = result
+ return Triple(res, resId, targetTone)
+ }
+
+ fun resolveResourceId(resStr: String): Pair<Resources, Int>? {
+ val (packageName, category, name) = parseResourceId(resStr)
+ return resolveResourceId(packageName, category, name)
+ }
+
+ fun resolveResourceId(
+ packageName: String?,
+ category: String,
+ name: String,
+ ): Pair<Resources, Int>? {
+ for ((res, ctxPkgName) in resources) {
+ val result = res.getIdentifier(name, category, packageName ?: ctxPkgName)
+ if (result != 0) {
+ return Pair(res, result)
+ }
+ }
+ return null
+ }
+
+ private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? {
+ try {
+ if (arg == null) {
+ return null
+ }
+ return fn(arg)
+ } catch (ex: IOException) {
+ logger.w("Failed to read $arg", ex)
+ return null
+ }
+ }
+
+ fun assetExists(path: String): Boolean {
+ try {
+ if (path.startsWith("@")) {
+ val pair = resolveResourceId(path)
+ val colorPair = resolveColorResourceId(path)
+ return pair != null || colorPair != null
+ } else {
+ val stream = pluginCtx.resources.assets.open("$baseDir$path")
+ if (stream == null) {
+ return false
+ }
+
+ stream.close()
+ return true
+ }
+ } catch (ex: IOException) {
+ return false
+ }
+ }
+
+ fun copy(messageBuffer: MessageBuffer? = null): AssetLoader =
+ AssetLoader(
+ pluginCtx,
+ sysuiCtx,
+ baseDir,
+ colorScheme,
+ seedColor,
+ overrideChroma,
+ typefaceCache,
+ getThemeSeedColor,
+ messageBuffer ?: logger.buffer,
+ )
+
+ fun setSeedColor(seedColor: Int?, style: MonetStyle?) {
+ this.seedColor = seedColor
+ refreshColorPalette(style)
+ }
+
+ fun refreshColorPalette(style: MonetStyle?) {
+ val seedColor =
+ this.seedColor ?: getThemeSeedColor(sysuiCtx).also { logColor("Theme Seed Color", it) }
+ this.colorScheme =
+ ColorScheme(
+ seedColor,
+ false, // darkTheme is not used for palette generation
+ style ?: MonetStyle.CLOCK,
+ )
+
+ // Enforce low chroma on output colors if low chroma theme is selected
+ this.overrideChroma = run {
+ val cam = colorScheme?.seed?.let { Cam.fromInt(it) }
+ if (cam != null && cam.chroma < LOW_CHROMA_LIMIT) {
+ return@run cam.chroma * LOW_CHROMA_SCALE
+ }
+ return@run null
+ }
+ }
+
+ fun getClockPaddingStart(): Int {
+ val result = resolveResourceId(null, "dimen", "clock_padding_start")
+ if (result != null) {
+ val (res, id) = result
+ return res.getDimensionPixelSize(id)
+ }
+ return -1
+ }
+
+ fun getStatusBarHeight(): Int {
+ val display = pluginCtx.getDisplayNoVerify()
+ if (display != null) {
+ return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout)
+ }
+
+ logger.w("No display available; falling back to android.R.dimen.status_bar_height")
+ val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height")
+ if (statusBarHeight != null) {
+ val (res, resId) = statusBarHeight
+ return res.getDimensionPixelSize(resId)
+ }
+
+ throw Exception("Could not fetch StatusBarHeight")
+ }
+
+ fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id }
+
+ fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize)
+
+ fun getString(name: String): String = getResource("string", name, Resources::getString)
+
+ private fun <T> getResource(
+ category: String,
+ name: String,
+ getter: (res: Resources, id: Int) -> T,
+ ): T {
+ val result = resolveResourceId(null, category, name)
+ if (result != null) {
+ val (res, id) = result
+ if (id == -1) throw Exception("Cannot find id of $id from $TAG")
+ return getter(res, id)
+ }
+ throw Exception("Cannot find id of $name from $TAG")
+ }
+
+ private fun logColor(name: String, color: Int) {
+ if (DEBUG_COLOR) {
+ val cam = Cam.fromInt(color)
+ val tone = CamUtils.lstarFromInt(color)
+ logger.i("$name -> (hue: ${cam.hue}, chroma: ${cam.chroma}, tone: $tone)")
+ }
+ }
+
+ companion object {
+ private val DEBUG_COLOR = true
+ private val LOW_CHROMA_LIMIT = 15
+ private val LOW_CHROMA_SCALE = 1.5f
+ private val TAG = AssetLoader::class.simpleName!!
+
+ private fun getThemeSeedColor(ctx: Context): Int {
+ return ctx.resources.getColor(android.R.color.system_palette_key_color_primary_light)
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt
new file mode 100644
index 0000000..5a04169
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockAnimation.kt
@@ -0,0 +1,21 @@
+/*
+ * 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
+
+object ClockAnimation {
+ const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
+}
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
new file mode 100644
index 0000000..f5e8432
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt
@@ -0,0 +1,288 @@
+/*
+ * 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,
+ val colorPalette: MonetStyle? = 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,
+ /**
+ * use `wdth` for width, `wght` for weight, 'opsz' for optical size single quote for tag name,
+ * and no quote for value separate different axis with `,` e.g. "'wght' 1000, 'wdth' 108, 'opsz'
+ * 90"
+ */
+ var fontVariation: String? = 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/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 954155d..9da3022 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -65,7 +65,7 @@
private fun <TKey : Any, TVal : Any> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut(
key: TKey,
value: TVal,
- onNew: (TVal) -> Unit
+ onNew: (TVal) -> Unit,
): TVal {
val result = this.putIfAbsent(key, value)
if (result == null) {
@@ -110,7 +110,7 @@
selfChange: Boolean,
uris: Collection<Uri>,
flags: Int,
- userId: Int
+ userId: Int,
) {
scope.launch(bgDispatcher) { querySettings() }
}
@@ -180,7 +180,7 @@
override fun onPluginLoaded(
plugin: ClockProviderPlugin,
pluginContext: Context,
- manager: PluginLifecycleManager<ClockProviderPlugin>
+ manager: PluginLifecycleManager<ClockProviderPlugin>,
) {
plugin.initialize(clockBuffers)
@@ -218,7 +218,7 @@
override fun onPluginUnloaded(
plugin: ClockProviderPlugin,
- manager: PluginLifecycleManager<ClockProviderPlugin>
+ manager: PluginLifecycleManager<ClockProviderPlugin>,
) {
for (clock in plugin.getClocks()) {
val id = clock.clockId
@@ -290,12 +290,12 @@
Settings.Secure.getStringForUser(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
- ActivityManager.getCurrentUser()
+ ActivityManager.getCurrentUser(),
)
} else {
Settings.Secure.getString(
context.contentResolver,
- Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+ Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
)
}
@@ -320,13 +320,13 @@
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
json,
- ActivityManager.getCurrentUser()
+ ActivityManager.getCurrentUser(),
)
} else {
Settings.Secure.putString(
context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE,
- json
+ json,
)
}
} catch (ex: Exception) {
@@ -418,7 +418,7 @@
pluginManager.addPluginListener(
pluginListener,
ClockProviderPlugin::class.java,
- /*allowMultiple=*/ true
+ /*allowMultiple=*/ true,
)
scope.launch(bgDispatcher) { querySettings() }
@@ -427,7 +427,7 @@
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
settingObserver,
- UserHandle.USER_ALL
+ UserHandle.USER_ALL,
)
ActivityManager.getService().registerUserSwitchObserver(userSwitchObserver, TAG)
@@ -435,7 +435,7 @@
context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
/*notifyForDescendants=*/ false,
- settingObserver
+ settingObserver,
)
}
}
@@ -504,7 +504,7 @@
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
- { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Connected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
@@ -516,7 +516,7 @@
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
- { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Loaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
@@ -532,7 +532,7 @@
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.WARNING else LogLevel.DEBUG,
- { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Unloaded $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
@@ -548,7 +548,7 @@
val isCurrent = currentClockId == info.metadata.clockId
logger.log(
if (isCurrent) LogLevel.INFO else LogLevel.DEBUG,
- { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" }
+ { "Disconnected $str1 @$str2" + if (bool1) " (Current Clock)" else "" },
) {
str1 = info.metadata.clockId
str2 = info.manager.toString()
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 4802e34..07191c6 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
@@ -34,7 +34,7 @@
val layoutInflater: LayoutInflater,
val resources: Resources,
val hasStepClockAnimation: Boolean = false,
- val migratedClocks: Boolean = false
+ val migratedClocks: Boolean = false,
) : ClockProvider {
private var messageBuffers: ClockMessageBuffers? = null
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
new file mode 100644
index 0000000..3869706
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.graphics.Point
+
+class DigitTranslateAnimator(val updateCallback: () -> Unit) {
+ val DEFAULT_ANIMATION_DURATION = 500L
+ val updatedTranslate = Point(0, 0)
+
+ val baseTranslation = Point(0, 0)
+ var targetTranslation: Point? = null
+ val bounceAnimator: ValueAnimator =
+ ValueAnimator.ofFloat(1f).apply {
+ duration = DEFAULT_ANIMATION_DURATION
+ addUpdateListener {
+ updateTranslation(it.animatedFraction, updatedTranslate)
+ updateCallback()
+ }
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ rebase()
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ rebase()
+ }
+ }
+ )
+ }
+
+ fun rebase() {
+ baseTranslation.x = updatedTranslate.x
+ baseTranslation.y = updatedTranslate.y
+ }
+
+ fun animatePosition(
+ animate: Boolean = true,
+ delay: Long = 0,
+ duration: Long = -1L,
+ interpolator: TimeInterpolator? = null,
+ targetTranslation: Point? = null,
+ onAnimationEnd: Runnable? = null,
+ ) {
+ this.targetTranslation = targetTranslation ?: Point(0, 0)
+ if (animate) {
+ bounceAnimator.cancel()
+ bounceAnimator.startDelay = delay
+ bounceAnimator.duration =
+ if (duration == -1L) {
+ DEFAULT_ANIMATION_DURATION
+ } else {
+ duration
+ }
+ interpolator?.let { bounceAnimator.interpolator = it }
+ if (onAnimationEnd != null) {
+ val listener =
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ onAnimationEnd.run()
+ bounceAnimator.removeListener(this)
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ bounceAnimator.removeListener(this)
+ }
+ }
+ bounceAnimator.addListener(listener)
+ }
+ bounceAnimator.start()
+ } else {
+ // No animation is requested, thus set base and target state to the same state.
+ updateTranslation(1F, updatedTranslate)
+ rebase()
+ updateCallback()
+ }
+ }
+
+ fun updateTranslation(progress: Float, outPoint: Point) {
+ outPoint.x =
+ (baseTranslation.x + progress * (targetTranslation!!.x - baseTranslation.x)).toInt()
+ outPoint.y =
+ (baseTranslation.y + progress * (targetTranslation!!.y - baseTranslation.y)).toInt()
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt
new file mode 100644
index 0000000..2be6c65
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DimensionParser.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.content.Context
+import android.util.TypedValue
+import java.util.regex.Pattern
+
+class DimensionParser(private val ctx: Context) {
+ fun convert(dimension: String?): Float? {
+ if (dimension == null) {
+ return null
+ }
+ return convert(dimension)
+ }
+
+ fun convert(dimension: String): Float {
+ val metrics = ctx.resources.displayMetrics
+ val (value, unit) = parse(dimension)
+ return TypedValue.applyDimension(unit, value, metrics)
+ }
+
+ fun parse(dimension: String): Pair<Float, Int> {
+ val matcher = parserPattern.matcher(dimension)
+ if (!matcher.matches()) {
+ throw NumberFormatException("Failed to parse '$dimension'")
+ }
+
+ val value =
+ matcher.group(1)?.toFloat() ?: throw NumberFormatException("Bad value in '$dimension'")
+ val unit =
+ dimensionMap.get(matcher.group(3) ?: "")
+ ?: throw NumberFormatException("Bad unit in '$dimension'")
+ return Pair(value, unit)
+ }
+
+ private companion object {
+ val parserPattern = Pattern.compile("(\\d+(\\.\\d+)?)([a-z]+)")
+ val dimensionMap =
+ mapOf(
+ "dp" to TypedValue.COMPLEX_UNIT_DIP,
+ "dip" to TypedValue.COMPLEX_UNIT_DIP,
+ "sp" to TypedValue.COMPLEX_UNIT_SP,
+ "px" to TypedValue.COMPLEX_UNIT_PX,
+ "pt" to TypedValue.COMPLEX_UNIT_PT,
+ "mm" to TypedValue.COMPLEX_UNIT_MM,
+ "in" to TypedValue.COMPLEX_UNIT_IN,
+ )
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt
new file mode 100644
index 0000000..34cb4ef
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LogUtil.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
+import com.android.systemui.log.core.Logger
+
+object LogUtil {
+ // Used when MessageBuffers are not provided by the host application
+ val DEFAULT_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.INFO)
+
+ // Only intended for use during initialization steps where the correct logger doesn't exist yet
+ val FALLBACK_INIT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.ERROR), "CLOCK_INIT")
+
+ // Debug is primarially used for tests, but can also be used for tracking down hard issues.
+ val DEBUG_MESSAGE_BUFFER = LogcatOnlyMessageBuffer(LogLevel.DEBUG)
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt
new file mode 100644
index 0000000..f5a9375
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TypefaceCache.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shared.clocks
+
+import android.graphics.Typeface
+import com.android.systemui.animation.TypefaceVariantCache
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.WeakReference
+
+class TypefaceCache(messageBuffer: MessageBuffer, val typefaceFactory: (String) -> Typeface) {
+ private val logger = Logger(messageBuffer, this::class.simpleName!!)
+
+ private data class CacheKey(val res: String, val fvar: String?)
+
+ private inner class WeakTypefaceRef(val key: CacheKey, typeface: Typeface) :
+ WeakReference<Typeface>(typeface, queue)
+
+ private var totalHits = 0
+
+ private var totalMisses = 0
+
+ private var totalEvictions = 0
+
+ // We use a map of WeakRefs here instead of an LruCache. This prevents needing to resize the
+ // cache depending on the number of distinct fonts used by a clock, as different clocks have
+ // different numbers of simultaneously loaded and configured fonts. Because our clocks tend to
+ // initialize a number of parallel views and animators, our usages of Typefaces overlap. As a
+ // result, once a typeface is no longer being used, it is unlikely to be recreated immediately.
+ private val cache = mutableMapOf<CacheKey, WeakTypefaceRef>()
+ private val queue = ReferenceQueue<Typeface>()
+
+ fun getTypeface(res: String): Typeface {
+ checkQueue()
+ val key = CacheKey(res, null)
+ cache.get(key)?.get()?.let {
+ logHit(key)
+ return it
+ }
+
+ logMiss(key)
+ val result = typefaceFactory(res)
+ cache.put(key, WeakTypefaceRef(key, result))
+ return result
+ }
+
+ fun getVariantCache(res: String): TypefaceVariantCache {
+ val baseTypeface = getTypeface(res)
+ return object : TypefaceVariantCache {
+ override fun getTypefaceForVariant(fvar: String?): Typeface? {
+ checkQueue()
+ val key = CacheKey(res, fvar)
+ cache.get(key)?.get()?.let {
+ logHit(key)
+ return it
+ }
+
+ logMiss(key)
+ return TypefaceVariantCache.createVariantTypeface(baseTypeface, fvar).also {
+ cache.put(key, WeakTypefaceRef(key, it))
+ }
+ }
+ }
+ }
+
+ private fun logHit(key: CacheKey) {
+ totalHits++
+ if (DEBUG_HITS)
+ logger.i({ "HIT: $str1; Total: $int1" }) {
+ str1 = key.toString()
+ int1 = totalHits
+ }
+ }
+
+ private fun logMiss(key: CacheKey) {
+ totalMisses++
+ logger.w({ "MISS: $str1; Total: $int1" }) {
+ str1 = key.toString()
+ int1 = totalMisses
+ }
+ }
+
+ private fun logEviction(key: CacheKey) {
+ totalEvictions++
+ logger.i({ "EVICTED: $str1; Total: $int1" }) {
+ str1 = key.toString()
+ int1 = totalEvictions
+ }
+ }
+
+ private fun checkQueue() =
+ generateSequence { queue.poll() }
+ .filterIsInstance<WeakTypefaceRef>()
+ .forEach {
+ logEviction(it.key)
+ cache.remove(it.key)
+ }
+
+ companion object {
+ private val DEBUG_HITS = false
+ }
+}
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
new file mode 100644
index 0000000..eb72346
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.content.Context
+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.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.LogUtil
+import java.util.Locale
+
+abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) {
+ protected val logger = Logger(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 updateColors(assets: AssetLoader, isRegionDark: Boolean) {
+ digitalClockTextViewMap.forEach { _, view -> view.updateColors(assets, isRegionDark) }
+ invalidate()
+ }
+
+ 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
+
+ // TODO: implement ClockEventUnion?
+ 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
new file mode 100644
index 0000000..c29c8da
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -0,0 +1,260 @@
+/*
+ * 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.content.Context
+import android.graphics.Canvas
+import android.graphics.Point
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import com.android.app.animation.Interpolators
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.DigitTranslateAnimator
+import com.android.systemui.shared.clocks.FontTextStyle
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
+
+class FlexClockView(context: Context, val assetLoader: AssetLoader, messageBuffer: MessageBuffer) :
+ DigitalClockFaceView(context, messageBuffer) {
+ override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
+ val digitLeftTopMap = mutableMapOf<Int, Point>()
+ var maxSingleDigitHeight = -1
+ var maxSingleDigitWidth = -1
+ val lockscreenTranslate = Point(0, 0)
+ val aodTranslate = Point(0, 0)
+
+ init {
+ setWillNotDraw(false)
+ layoutParams =
+ RelativeLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ )
+ }
+
+ private var prevX = 0f
+ private var prevY = 0f
+ private var isDown = false
+
+ // TODO(b/340253296): Genericize; json spec
+ private var wght = 603f
+ private var wdth = 100f
+
+ // TODO(b/340253296): Json spec
+ private val MAX_WGHT = 950f
+ private val MIN_WGHT = 50f
+ private val WGHT_SCALE = 0.5f
+
+ private val MAX_WDTH = 150f
+ private val MIN_WDTH = 0f
+ private val WDTH_SCALE = 0.2f
+
+ override fun onTouchEvent(evt: MotionEvent): Boolean {
+ // TODO(b/340253296): implement on DigitalClockFaceView?
+ if (!isReactiveTouchInteractionEnabled) {
+ return super.onTouchEvent(evt)
+ }
+
+ when (evt.action) {
+ MotionEvent.ACTION_DOWN -> {
+ isDown = true
+ prevX = evt.x
+ prevY = evt.y
+ return true
+ }
+
+ MotionEvent.ACTION_MOVE -> {
+ if (!isDown) {
+ return super.onTouchEvent(evt)
+ }
+
+ wdth = clamp(wdth + (evt.x - prevX) * WDTH_SCALE, MIN_WDTH, MAX_WDTH)
+ wght = clamp(wght + (evt.y - prevY) * WGHT_SCALE, MIN_WGHT, MAX_WGHT)
+ prevX = evt.x
+ prevY = evt.y
+
+ // TODO(b/340253296): Genericize; json spec
+ val fvar = "'wght' $wght, 'wdth' $wdth, 'opsz' 144, 'ROND' 100"
+ digitalClockTextViewMap.forEach { (_, view) ->
+ val textStyle = view.textStyle as FontTextStyle
+ textStyle.fontVariation = fvar
+ view.applyStyles(assetLoader, textStyle, view.aodStyle)
+ }
+
+ requestLayout()
+ invalidate()
+ return true
+ }
+
+ MotionEvent.ACTION_UP -> {
+ isDown = false
+ return true
+ }
+ }
+
+ return super.onTouchEvent(evt)
+ }
+
+ override fun addView(child: View?) {
+ super.addView(child)
+ (child as SimpleDigitalClockTextView).digitTranslateAnimator =
+ DigitTranslateAnimator(::invalidate)
+ }
+
+ protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point {
+ digitalClockTextViewMap.forEach { (_, textView) ->
+ textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ }
+ val textView = digitalClockTextViewMap[R.id.HOUR_FIRST_DIGIT]!!
+ maxSingleDigitHeight = textView.measuredHeight
+ maxSingleDigitWidth = textView.measuredWidth
+ aodTranslate.x = -(maxSingleDigitWidth * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt()
+ aodTranslate.y = (maxSingleDigitHeight * AOD_VERTICAL_TRANSLATE_RATIO).toInt()
+ return Point(
+ ((maxSingleDigitWidth + abs(aodTranslate.x)) * 2),
+ ((maxSingleDigitHeight + abs(aodTranslate.y)) * 2),
+ )
+ }
+
+ protected override fun calculateLeftTopPosition() {
+ digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0)
+ digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitWidth, 0)
+ digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitHeight)
+ digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitWidth, maxSingleDigitHeight)
+ digitLeftTopMap.forEach { _, point ->
+ point.x += abs(aodTranslate.x)
+ point.y += abs(aodTranslate.y)
+ }
+ }
+
+ override fun refreshTime() {
+ super.refreshTime()
+ digitalClockTextViewMap.forEach { (_, textView) -> textView.refreshText() }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ digitalClockTextViewMap.forEach { (id, _) ->
+ val textView = digitalClockTextViewMap[id]!!
+ canvas.translate(digitLeftTopMap[id]!!.x.toFloat(), digitLeftTopMap[id]!!.y.toFloat())
+ textView.draw(canvas)
+ canvas.translate(-digitLeftTopMap[id]!!.x.toFloat(), -digitLeftTopMap[id]!!.y.toFloat())
+ }
+ }
+
+ override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ dozeControlState.animateDoze = {
+ super.animateDoze(isDozing, isAnimated)
+ if (maxSingleDigitHeight == -1) {
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ }
+ digitalClockTextViewMap.forEach { (id, textView) ->
+ textView.digitTranslateAnimator?.let {
+ if (!isDozing) {
+ it.animatePosition(
+ animate = isAnimated && isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = AOD_TRANSITION_DURATION,
+ targetTranslation =
+ updateDirectionalTargetTranslate(id, lockscreenTranslate),
+ )
+ } else {
+ it.animatePosition(
+ animate = isAnimated && isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = AOD_TRANSITION_DURATION,
+ onAnimationEnd = null,
+ targetTranslation = updateDirectionalTargetTranslate(id, aodTranslate),
+ )
+ }
+ }
+ }
+ }
+ }
+
+ override fun animateCharge() {
+ super.animateCharge()
+ digitalClockTextViewMap.forEach { (id, textView) ->
+ textView.digitTranslateAnimator?.let {
+ it.animatePosition(
+ animate = isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = CHARGING_TRANSITION_DURATION,
+ onAnimationEnd = {
+ it.animatePosition(
+ animate = isAnimationEnabled,
+ interpolator = Interpolators.EMPHASIZED,
+ duration = CHARGING_TRANSITION_DURATION,
+ targetTranslation =
+ updateDirectionalTargetTranslate(
+ id,
+ if (dozeFraction == 1F) aodTranslate else lockscreenTranslate,
+ ),
+ )
+ },
+ targetTranslation =
+ updateDirectionalTargetTranslate(
+ id,
+ if (dozeFraction == 1F) lockscreenTranslate else aodTranslate,
+ ),
+ )
+ }
+ }
+ }
+
+ companion object {
+ val AOD_TRANSITION_DURATION = 750L
+ val CHARGING_TRANSITION_DURATION = 300L
+
+ val AOD_HORIZONTAL_TRANSLATE_RATIO = 0.15F
+ val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F
+
+ // Use the sign of targetTranslation to control the direction of digit translation
+ fun updateDirectionalTargetTranslate(id: Int, targetTranslation: Point): Point {
+ val outPoint = Point(targetTranslation)
+ when (id) {
+ R.id.HOUR_FIRST_DIGIT -> {
+ outPoint.x *= -1
+ outPoint.y *= -1
+ }
+
+ R.id.HOUR_SECOND_DIGIT -> {
+ outPoint.x *= 1
+ outPoint.y *= -1
+ }
+
+ R.id.MINUTE_FIRST_DIGIT -> {
+ outPoint.x *= -1
+ outPoint.y *= 1
+ }
+
+ R.id.MINUTE_SECOND_DIGIT -> {
+ outPoint.x *= 1
+ outPoint.y *= 1
+ }
+ }
+ return outPoint
+ }
+ }
+}
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
new file mode 100644
index 0000000..74617b1
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -0,0 +1,654 @@
+/*
+ * 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.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Point
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.Rect
+import android.text.Layout
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.util.Log
+import android.util.MathUtils
+import android.util.TypedValue
+import android.view.View.MeasureSpec.AT_MOST
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.animation.Interpolator
+import android.widget.TextView
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.TextAnimator
+import com.android.systemui.animation.TypefaceVariantCache
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.ClockAnimation
+import com.android.systemui.shared.clocks.DigitTranslateAnimator
+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.ceil
+import kotlin.math.max
+import kotlin.math.min
+
+private val TAG = SimpleDigitalClockTextView::class.simpleName!!
+
+@SuppressLint("AppCompatCustomView")
+open class SimpleDigitalClockTextView(
+ ctx: Context,
+ messageBuffer: MessageBuffer,
+ attrs: AttributeSet? = null,
+) : TextView(ctx, attrs), SimpleDigitalClockView {
+ val lockScreenPaint = TextPaint()
+ override lateinit var textStyle: FontTextStyle
+ lateinit var aodStyle: FontTextStyle
+ private val parser = DimensionParser(ctx)
+ var maxSingleDigitHeight = -1
+ var maxSingleDigitWidth = -1
+ var digitTranslateAnimator: DigitTranslateAnimator? = null
+ var aodFontSizePx: Float = -1F
+ var isVertical: Boolean = false
+
+ // Store the font size when there's no height constraint as a reference when adjusting font size
+ private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
+ // Calculated by height of styled text view / text size
+ // Used as a factor to calculate a smaller font size when text height is constrained
+ @VisibleForTesting var fontSizeAdjustFactor = 1F
+
+ private val initThread = Thread.currentThread()
+
+ // textBounds is the size of text in LS, which only measures current text in lockscreen style
+ var textBounds = Rect()
+ // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
+ // especially for the textView which has different bounds during the animation
+ // prevTextBounds holds the state we are transitioning from
+ private val prevTextBounds = Rect()
+ // targetTextBounds holds the state we are interpolating to
+ private val targetTextBounds = Rect()
+ protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+ get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
+
+ private var aodDozingInterpolator: Interpolator? = null
+
+ @VisibleForTesting lateinit var textAnimator: TextAnimator
+ @VisibleForTesting var outlineAnimator: TextAnimator? = null
+ // used for hollow style for AOD version
+ // because stroke style for some fonts have some unwanted inner strokes
+ // we want to draw this layer on top to oclude them
+ @VisibleForTesting var innerAnimator: TextAnimator? = null
+
+ lateinit var typefaceCache: TypefaceVariantCache
+ private set
+
+ private fun setTypefaceCache(value: TypefaceVariantCache) {
+ typefaceCache = value
+ if (this::textAnimator.isInitialized) {
+ textAnimator.typefaceCache = value
+ }
+ outlineAnimator?.typefaceCache = value
+ innerAnimator?.typefaceCache = value
+ }
+
+ @VisibleForTesting
+ var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
+ TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also {
+ if (this::typefaceCache.isInitialized) {
+ it.typefaceCache = typefaceCache
+ }
+ }
+ }
+
+ override var verticalAlignment: VerticalAlignment = VerticalAlignment.CENTER
+ override var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.LEFT
+ override var isAnimationEnabled = true
+ override 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 aodBorderWidth = 0F
+ var baselineFromMeasure = 0
+
+ var textFillColor: Int? = null
+ var textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR
+ var aodFillColor = AOD_DEFAULT_COLOR
+ var aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR
+
+ override fun updateColors(assets: AssetLoader, isRegionDark: Boolean) {
+ val fillColor = if (isRegionDark) textStyle.fillColorLight else textStyle.fillColorDark
+ textFillColor =
+ fillColor?.let { assets.readColor(it) }
+ ?: assets.seedColor
+ ?: getDefaultColor(assets, isRegionDark)
+ // for NumberOverlapView to read correct color
+ lockScreenPaint.color = textFillColor as Int
+ textStyle.outlineColor?.let { textOutlineColor = assets.readColor(it) }
+ ?: run { textOutlineColor = TEXT_OUTLINE_DEFAULT_COLOR }
+ (aodStyle.fillColorLight ?: aodStyle.fillColorDark)?.let {
+ aodFillColor = assets.readColor(it)
+ } ?: run { aodFillColor = AOD_DEFAULT_COLOR }
+ aodStyle.outlineColor?.let { aodOutlineColor = assets.readColor(it) }
+ ?: run { aodOutlineColor = AOD_OUTLINE_DEFAULT_COLOR }
+ if (dozeFraction < 1f) {
+ textAnimator.setTextStyle(color = textFillColor, animate = false)
+ outlineAnimator?.setTextStyle(color = textOutlineColor, animate = false)
+ }
+ invalidate()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ logger.d("onMeasure()")
+ if (isVertical) {
+ // use at_most to avoid apply measuredWidth from last measuring to measuredHeight
+ // cause we use max to setMeasuredDimension
+ super.onMeasure(
+ MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), AT_MOST),
+ MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), AT_MOST),
+ )
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
+ val layout = this.layout
+ if (layout != null) {
+ if (!this::textAnimator.isInitialized) {
+ textAnimator = textAnimatorFactory(layout, ::invalidate)
+ outlineAnimator = textAnimatorFactory(layout) {}
+ innerAnimator = textAnimatorFactory(layout) {}
+ setInterpolatorPaint()
+ } else {
+ textAnimator.updateLayout(layout)
+ outlineAnimator?.updateLayout(layout)
+ innerAnimator?.updateLayout(layout)
+ }
+ baselineFromMeasure = layout.getLineBaseline(0)
+ } else {
+ val currentThread = Thread.currentThread()
+ Log.wtf(
+ TAG,
+ "TextView.getLayout() is null after measure! " +
+ "currentThread=$currentThread; initThread=$initThread",
+ )
+ }
+
+ var expectedWidth: Int
+ var expectedHeight: Int
+
+ if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+ // For view which has fixed height, e.g. small clock,
+ // we should always return the size required from parent view
+ expectedHeight = heightMeasureSpec
+ } else {
+ expectedHeight =
+ MeasureSpec.makeMeasureSpec(
+ if (isSingleDigit()) {
+ maxSingleDigitHeight
+ } else {
+ textBounds.height() + 2 * lockScreenPaint.strokeWidth.toInt()
+ },
+ MeasureSpec.getMode(measuredHeight),
+ )
+ }
+ if (MeasureSpec.getMode(widthMeasureSpec) == EXACTLY) {
+ expectedWidth = widthMeasureSpec
+ } else {
+ expectedWidth =
+ MeasureSpec.makeMeasureSpec(
+ if (isSingleDigit()) {
+ maxSingleDigitWidth
+ } else {
+ max(
+ textBounds.width() + 2 * lockScreenPaint.strokeWidth.toInt(),
+ MeasureSpec.getSize(measuredWidth),
+ )
+ },
+ MeasureSpec.getMode(measuredWidth),
+ )
+ }
+
+ if (isVertical) {
+ expectedWidth = expectedHeight.also { expectedHeight = expectedWidth }
+ }
+ setMeasuredDimension(expectedWidth, expectedHeight)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ if (isVertical) {
+ canvas.save()
+ canvas.translate(0F, measuredHeight.toFloat())
+ canvas.rotate(-90F)
+ }
+ logger.d({ "onDraw(); ls: $str1; aod: $str2;" }) {
+ str1 = textAnimator.textInterpolator.shapedText
+ str2 = outlineAnimator?.textInterpolator?.shapedText
+ }
+ val translation = getLocalTranslation()
+ canvas.translate(translation.x.toFloat(), translation.y.toFloat())
+ digitTranslateAnimator?.let {
+ 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,
+ )
+ outlineAnimator?.draw(canvas)
+ canvas.saveLayer(
+ -translation.x.toFloat(),
+ -translation.y.toFloat(),
+ (-translation.x + measuredWidth).toFloat(),
+ (-translation.y + measuredHeight).toFloat(),
+ Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) },
+ )
+ innerAnimator?.draw(canvas)
+ canvas.restore()
+ canvas.restore()
+ } else if (aodStyle.renderType != RenderType.CHANGE_WEIGHT) {
+ outlineAnimator?.draw(canvas)
+ }
+ textAnimator.draw(canvas)
+
+ digitTranslateAnimator?.let {
+ canvas.translate(-it.updatedTranslate.x.toFloat(), -it.updatedTranslate.y.toFloat())
+ }
+ canvas.translate(-translation.x.toFloat(), -translation.y.toFloat())
+ if (isVertical) {
+ canvas.restore()
+ }
+ }
+
+ override fun invalidate() {
+ logger.d("invalidate()")
+ super.invalidate()
+ (parent as? DigitalClockFaceView)?.invalidate()
+ }
+
+ override fun refreshTime() {
+ logger.d("refreshTime()")
+ refreshText()
+ }
+
+ override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ if (!this::textAnimator.isInitialized) {
+ return
+ }
+ val fvar = if (isDozing) aodStyle.fontVariation else textStyle.fontVariation
+ textAnimator.setTextStyle(
+ animate = isAnimated && isAnimationEnabled,
+ color = if (isDozing) aodFillColor else textFillColor,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ fvar = fvar,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ )
+ updateTextBoundsForTextAnimator()
+ outlineAnimator?.setTextStyle(
+ animate = isAnimated && isAnimationEnabled,
+ color = if (isDozing) aodOutlineColor else textOutlineColor,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ fvar = fvar,
+ strokeWidth = if (isDozing) aodBorderWidth else textBorderWidth,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ )
+ innerAnimator?.setTextStyle(
+ animate = isAnimated && isAnimationEnabled,
+ color = Color.WHITE,
+ textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
+ fvar = fvar,
+ duration = aodStyle.transitionDuration,
+ interpolator = aodDozingInterpolator,
+ )
+ }
+
+ override fun animateCharge() {
+ if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+ // Skip charge animation if dozing animation is already playing.
+ return
+ }
+ logger.d("animateCharge()")
+ val middleFvar = if (dozeFraction == 0F) aodStyle.fontVariation else textStyle.fontVariation
+ val endFvar = if (dozeFraction == 0F) textStyle.fontVariation else aodStyle.fontVariation
+ val startAnimPhase2 = Runnable {
+ textAnimator.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+ outlineAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+ innerAnimator?.setTextStyle(fvar = endFvar, animate = isAnimationEnabled)
+ updateTextBoundsForTextAnimator()
+ }
+ textAnimator.setTextStyle(
+ fvar = middleFvar,
+ animate = isAnimationEnabled,
+ onAnimationEnd = startAnimPhase2,
+ )
+ outlineAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled)
+ innerAnimator?.setTextStyle(fvar = middleFvar, animate = isAnimationEnabled)
+ updateTextBoundsForTextAnimator()
+ }
+
+ fun refreshText() {
+ lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
+ if (this::textAnimator.isInitialized) {
+ textAnimator.textInterpolator.targetPaint.getTextBounds(
+ text,
+ 0,
+ text.length,
+ targetTextBounds,
+ )
+ }
+ if (layout == null) {
+ requestLayout()
+ } else {
+ textAnimator.updateLayout(layout)
+ outlineAnimator?.updateLayout(layout)
+ innerAnimator?.updateLayout(layout)
+ }
+ }
+
+ private fun isSingleDigit(): Boolean {
+ return id == R.id.HOUR_FIRST_DIGIT ||
+ id == R.id.HOUR_SECOND_DIGIT ||
+ id == R.id.MINUTE_FIRST_DIGIT ||
+ id == R.id.MINUTE_SECOND_DIGIT
+ }
+
+ private fun updateInterpolatedTextBounds(): Rect {
+ val interpolatedTextBounds = Rect()
+ if (textAnimator.animator.animatedFraction != 1.0f && textAnimator.animator.isRunning) {
+ interpolatedTextBounds.left =
+ MathUtils.lerp(
+ prevTextBounds.left,
+ targetTextBounds.left,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+
+ interpolatedTextBounds.right =
+ MathUtils.lerp(
+ prevTextBounds.right,
+ targetTextBounds.right,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+
+ interpolatedTextBounds.top =
+ MathUtils.lerp(
+ prevTextBounds.top,
+ targetTextBounds.top,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+
+ interpolatedTextBounds.bottom =
+ MathUtils.lerp(
+ prevTextBounds.bottom,
+ targetTextBounds.bottom,
+ textAnimator.animator.animatedValue as Float,
+ )
+ .toInt()
+ } else {
+ interpolatedTextBounds.set(targetTextBounds)
+ }
+ return interpolatedTextBounds
+ }
+
+ private fun updateXtranslation(inPoint: Point, interpolatedTextBounds: Rect): Point {
+ val viewWidth = if (isVertical) measuredHeight else measuredWidth
+ when (horizontalAlignment) {
+ HorizontalAlignment.LEFT -> {
+ inPoint.x = lockScreenPaint.strokeWidth.toInt() - interpolatedTextBounds.left
+ }
+ HorizontalAlignment.RIGHT -> {
+ inPoint.x =
+ viewWidth - interpolatedTextBounds.right - lockScreenPaint.strokeWidth.toInt()
+ }
+ HorizontalAlignment.CENTER -> {
+ inPoint.x =
+ (viewWidth - interpolatedTextBounds.width()) / 2 - interpolatedTextBounds.left
+ }
+ }
+ return inPoint
+ }
+
+ // translation of reference point of text
+ // used for translation when calling textInterpolator
+ fun getLocalTranslation(): Point {
+ val viewHeight = if (isVertical) measuredWidth else measuredHeight
+ val interpolatedTextBounds = updateInterpolatedTextBounds()
+ val localTranslation = Point(0, 0)
+ val correctedBaseline = if (baseline != -1) baseline else baselineFromMeasure
+ // get the change from current baseline to expected baseline
+ when (verticalAlignment) {
+ VerticalAlignment.CENTER -> {
+ localTranslation.y =
+ ((viewHeight - interpolatedTextBounds.height()) / 2 -
+ interpolatedTextBounds.top -
+ correctedBaseline)
+ }
+ VerticalAlignment.TOP -> {
+ localTranslation.y =
+ (-interpolatedTextBounds.top + lockScreenPaint.strokeWidth - correctedBaseline)
+ .toInt()
+ }
+ VerticalAlignment.BOTTOM -> {
+ localTranslation.y =
+ viewHeight -
+ interpolatedTextBounds.bottom -
+ lockScreenPaint.strokeWidth.toInt() -
+ correctedBaseline
+ }
+ VerticalAlignment.BASELINE -> {
+ localTranslation.y = -lockScreenPaint.strokeWidth.toInt()
+ }
+ }
+
+ return updateXtranslation(localTranslation, interpolatedTextBounds)
+ }
+
+ override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) {
+ this.textStyle = textStyle as FontTextStyle
+ val typefaceName = "fonts/" + textStyle.fontFamily
+ setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName))
+ lockScreenPaint.strokeJoin = Paint.Join.ROUND
+ lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(textStyle.fontVariation)
+ 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 }
+ aodBorderWidth = parser.convert(this.aodStyle.borderWidth ?: DEFAULT_AOD_STROKE_WIDTH)
+ lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth))
+ measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
+ setInterpolatorPaint()
+ recomputeMaxSingleDigitSizes()
+ invalidate()
+ }
+
+ // When constrainedByHeight is on, targetFontSizePx is the constrained height of textView
+ override fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean) {
+ val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
+ val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
+ aodFontSizePx =
+ adjustedFontSizePx * (aodStyle.fontSizeScale ?: textStyle.fontSizeScale ?: 1f)
+ if (fontSizePx > 0) {
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
+ lockScreenPaint.textSize = textSize
+ lockScreenPaint.getTextBounds(text, 0, text.length, textBounds)
+ targetTextBounds.set(textBounds)
+ }
+ if (!constrainedByHeight) {
+ val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+ fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
+ }
+ textStyle.borderWidthScale?.let {
+ textBorderWidth = fontSizePx * it
+ if (dozeFraction < 1.0F) {
+ outlineAnimator?.setTextStyle(strokeWidth = textBorderWidth, animate = false)
+ }
+ }
+ aodStyle.borderWidthScale?.let {
+ aodBorderWidth = fontSizePx * it
+ if (dozeFraction > 0.0F) {
+ outlineAnimator?.setTextStyle(strokeWidth = aodBorderWidth, animate = false)
+ }
+ }
+
+ lockScreenPaint.strokeWidth = ceil(max(textBorderWidth, aodBorderWidth))
+ recomputeMaxSingleDigitSizes()
+
+ if (this::textAnimator.isInitialized) {
+ textAnimator.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ }
+ outlineAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ innerAnimator?.setTextStyle(textSize = lockScreenPaint.textSize, animate = false)
+ }
+
+ private fun recomputeMaxSingleDigitSizes() {
+ val rectForCalculate = Rect()
+ maxSingleDigitHeight = 0
+ maxSingleDigitWidth = 0
+
+ for (i in 0..9) {
+ lockScreenPaint.getTextBounds(i.toString(), 0, 1, rectForCalculate)
+ maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
+ maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+ }
+ maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth.toInt()
+ maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth.toInt()
+ }
+
+ // called without animation, can be used to set the initial state of animator
+ private fun setInterpolatorPaint() {
+ if (this::textAnimator.isInitialized) {
+ // set initial style
+ textAnimator.textInterpolator.targetPaint.set(lockScreenPaint)
+ textAnimator.textInterpolator.onTargetPaintModified()
+ textAnimator.setTextStyle(
+ fvar = textStyle.fontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = textFillColor,
+ animate = false,
+ )
+ }
+
+ if (outlineAnimator != null) {
+ outlineAnimator!!
+ .textInterpolator
+ .targetPaint
+ .set(
+ TextPaint(lockScreenPaint).also {
+ it.style =
+ if (aodStyle.renderType == RenderType.HOLLOW_TEXT)
+ Paint.Style.FILL_AND_STROKE
+ else Paint.Style.STROKE
+ }
+ )
+ outlineAnimator!!.textInterpolator.onTargetPaintModified()
+ outlineAnimator!!.setTextStyle(
+ fvar = aodStyle.fontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = Color.TRANSPARENT,
+ animate = false,
+ )
+ }
+
+ if (innerAnimator != null) {
+ innerAnimator!!
+ .textInterpolator
+ .targetPaint
+ .set(TextPaint(lockScreenPaint).also { it.style = Paint.Style.FILL })
+ innerAnimator!!.textInterpolator.onTargetPaintModified()
+ innerAnimator!!.setTextStyle(
+ fvar = aodStyle.fontVariation,
+ textSize = lockScreenPaint.textSize,
+ color = Color.WHITE,
+ animate = false,
+ )
+ }
+ }
+
+ /* Called after textAnimator.setTextStyle
+ * textAnimator.setTextStyle will update targetPaint,
+ * and rebase if previous animator is canceled
+ * so basePaint will store the state we transition from
+ * and targetPaint will store the state we transition to
+ */
+ private fun updateTextBoundsForTextAnimator() {
+ textAnimator.textInterpolator.basePaint.getTextBounds(text, 0, text.length, prevTextBounds)
+ textAnimator.textInterpolator.targetPaint.getTextBounds(
+ text,
+ 0,
+ text.length,
+ targetTextBounds,
+ )
+ }
+
+ /*
+ * Adjust text size to adapt to large display / font size
+ * where the text view will be constrained by height
+ */
+ private fun adjustFontSize(targetFontSizePx: Float?, constrainedByHeight: Boolean): Float {
+ return if (constrainedByHeight) {
+ min((targetFontSizePx ?: 0F) / fontSizeAdjustFactor, lastUnconstrainedTextSize)
+ } else {
+ lastUnconstrainedTextSize = targetFontSizePx ?: 1F
+ lastUnconstrainedTextSize
+ }
+ }
+
+ companion object {
+ val DEFAULT_AOD_STROKE_WIDTH = "2dp"
+ val TEXT_OUTLINE_DEFAULT_COLOR = Color.TRANSPARENT
+ val AOD_DEFAULT_COLOR = Color.TRANSPARENT
+ val AOD_OUTLINE_DEFAULT_COLOR = Color.WHITE
+ private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0"
+ private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0"
+
+ fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) =
+ assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR)
+ }
+}
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
new file mode 100644
index 0000000..bbd2d3d
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.shared.clocks.AssetLoader
+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(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?)
+
+ fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
+
+ fun updateColors(assets: AssetLoader, isRegionDark: Boolean)
+
+ 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/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationFrameSizePrefsTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt
index 791a26e..6f43c20 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/WindowMagnificationFrameSpecTest.kt
@@ -16,9 +16,9 @@
package com.android.systemui.accessibility
-import android.testing.AndroidTestingRunner
import android.util.Size
import androidx.test.filters.SmallTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.WindowMagnificationSettings.MagnificationSize
import com.google.common.truth.Truth.assertThat
@@ -26,7 +26,7 @@
import org.junit.runner.RunWith
@SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
class WindowMagnificationFrameSpecTest : SysuiTestCase() {
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/fontscaling
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/fontscaling
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/battery/BatteryMeterViewTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 65825b2..2dcbdc8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.view.Display.INVALID_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
@@ -68,10 +69,12 @@
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
import android.os.Handler;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.os.UserManager;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
+import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
import android.view.WindowManager;
@@ -210,6 +213,7 @@
.thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
.thenReturn(true);
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(false);
when(mDialog1.getOpPackageName()).thenReturn("Dialog1");
when(mDialog2.getOpPackageName()).thenReturn("Dialog2");
@@ -462,7 +466,7 @@
@Test
public void testShowInvoked_whenSystemRequested() {
showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
- verify(mDialog1).show(any());
+ verify(mDialog1).show(mWindowManager);
}
@Test
@@ -679,7 +683,7 @@
// 2) Client cancels authentication
showDialog(new int[0] /* sensorIds */, true /* credentialAllowed */);
- verify(mDialog1).show(any());
+ verify(mDialog1).show(mWindowManager);
final byte[] credentialAttestation = generateRandomHAT();
@@ -695,7 +699,7 @@
@Test
public void testShowNewDialog_beforeOldDialogDismissed_SkipsAnimations() {
showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
- verify(mDialog1).show(any());
+ verify(mDialog1).show(mWindowManager);
showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);
@@ -703,7 +707,7 @@
verify(mDialog1).dismissWithoutCallback(eq(false) /* animate */);
// Second dialog should be shown without animation
- verify(mDialog2).show(any());
+ verify(mDialog2).show(mWindowManager);
}
@Test
@@ -990,13 +994,97 @@
verify(mDialog1, never()).show(any());
}
+ @Test
+ public void testShowDialog_visibleBackgroundUser() {
+ int backgroundUserId = 1001;
+ int backgroundDisplayId = 1001;
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+ WindowManager wm = mockBackgroundUser(backgroundUserId, backgroundDisplayId,
+ true /* isVisible */, true /* hasUserManager */, true /* hasDisplay */);
+
+ showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
+ false /* credentialAllowed */);
+
+ verify(mDialog1).show(wm);
+ }
+
+ @Test
+ public void testShowDialog_invisibleBackgroundUser_defaultWM() {
+ int backgroundUserId = 1001;
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+ mockBackgroundUser(backgroundUserId, INVALID_DISPLAY,
+ false /* isVisible */, true /* hasUserManager */, true /* hasDisplay */);
+
+ showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
+ false /* credentialAllowed */);
+
+ verify(mDialog1).show(mWindowManager);
+ }
+
+ @Test
+ public void testShowDialog_visibleBackgroundUser_noUserManager_dismissError()
+ throws RemoteException {
+ int backgroundUserId = 1001;
+ int backgroundDisplayId = 1001;
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+ mockBackgroundUser(backgroundUserId, backgroundDisplayId,
+ true /* isVisible */, false /* hasUserManager */, true /* hasDisplay */);
+
+ showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
+ false /* credentialAllowed */);
+
+ verify(mDialog1, never()).show(any());
+ verify(mReceiver).onDialogDismissed(
+ eq(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM),
+ eq(null) /* credentialAttestation */);
+ }
+
+ @Test
+ public void testShowDialog_visibleBackgroundUser_invalidDisplayId_dismissError()
+ throws RemoteException {
+ int backgroundUserId = 1001;
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+ mockBackgroundUser(backgroundUserId, INVALID_DISPLAY,
+ true /* isVisible */, true /* hasUserManager */, false /* hasDisplay */);
+
+ showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
+ false /* credentialAllowed */);
+
+ verify(mDialog1, never()).show(any());
+ verify(mReceiver).onDialogDismissed(
+ eq(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM),
+ eq(null) /* credentialAttestation */);
+ }
+
+ @Test
+ public void testShowDialog_visibleBackgroundUser_invalidDisplay_dismissError()
+ throws RemoteException {
+ int backgroundUserId = 1001;
+ int backgroundDisplayId = 1001;
+ when(mUserManager.isVisibleBackgroundUsersSupported()).thenReturn(true);
+ mockBackgroundUser(backgroundUserId, backgroundDisplayId,
+ true /* isVisible */, true /* hasUserManager */, false /* hasDisplay */);
+
+ showDialog(new int[]{1} /* sensorIds */, backgroundUserId /* userId */,
+ false /* credentialAllowed */);
+
+ verify(mDialog1, never()).show(any());
+ verify(mReceiver).onDialogDismissed(
+ eq(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM),
+ eq(null) /* credentialAttestation */);
+ }
+
private void showDialog(int[] sensorIds, boolean credentialAllowed) {
+ showDialog(sensorIds, 0 /* userId */, credentialAllowed);
+ }
+
+ private void showDialog(int[] sensorIds, int userId, boolean credentialAllowed) {
mAuthController.showAuthenticationDialog(createTestPromptInfo(),
mReceiver /* receiver */,
sensorIds,
credentialAllowed,
true /* requireConfirmation */,
- 0 /* userId */,
+ userId /* userId */,
0 /* operationId */,
"testPackage",
REQUEST_ID);
@@ -1059,6 +1147,40 @@
assertTrue(mAuthController.isFaceAuthEnrolled(userId));
}
+ /**
+ * Create mocks related to visible background users.
+ *
+ * @param userId the user id of the background user to mock
+ * @param displayId display id of the background user
+ * @param isVisible whether the background user is a visible background user or not
+ * @param hasUserManager simulate whether the background user's context will return a mock
+ * UserManager instance or null
+ * @param hasDisplay simulate whether the background user's context will return a mock Display
+ * instance or null
+ * @return mock WindowManager instance associated with the background user's display context
+ */
+ private WindowManager mockBackgroundUser(int userId, int displayId, boolean isVisible,
+ boolean hasUserManager, boolean hasDisplay) {
+ Context mockUserContext = mock(Context.class);
+ Context mockDisplayContext = mock(Context.class);
+ UserManager mockUserManager = mock(UserManager.class);
+ Display mockDisplay = mock(Display.class);
+ WindowManager mockDisplayWM = mock(WindowManager.class);
+ doReturn(mockUserContext).when(mContextSpy).createContextAsUser(eq(UserHandle.of(userId)),
+ anyInt());
+ if (hasUserManager) {
+ when(mockUserContext.getSystemService(UserManager.class)).thenReturn(mockUserManager);
+ }
+ when(mockUserManager.isUserVisible()).thenReturn(isVisible);
+ when(mockUserManager.getMainDisplayIdAssignedToUser()).thenReturn(displayId);
+ if (hasDisplay) {
+ when(mDisplayManager.getDisplay(displayId)).thenReturn(mockDisplay);
+ }
+ doReturn(mockDisplayContext).when(mContextSpy).createDisplayContext(mockDisplay);
+ when(mockDisplayContext.getSystemService(WindowManager.class)).thenReturn(mockDisplayWM);
+ return mockDisplayWM;
+ }
+
private final class TestableAuthController extends AuthController {
private int mBuildCount = 0;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 75a77cf..194b41f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -27,12 +27,23 @@
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.SysuiTestableContext
+import com.android.systemui.biometrics.data.repository.biometricStatusRepository
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
/** Create [FingerprintSensorPropertiesInternal] for a test. */
internal fun fingerprintSensorPropertiesInternal(
ids: List<Int> = listOf(0),
strong: Boolean = true,
- sensorType: Int = FingerprintSensorProperties.TYPE_REAR
+ sensorType: Int = FingerprintSensorProperties.TYPE_REAR,
): List<FingerprintSensorPropertiesInternal> {
val componentInfo =
listOf(
@@ -41,15 +52,15 @@
"vendor/model/revision" /* hardwareVersion */,
"1.01" /* firmwareVersion */,
"00000001" /* serialNumber */,
- "" /* softwareVersion */
+ "", /* softwareVersion */
),
ComponentInfoInternal(
"matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */,
"" /* firmwareVersion */,
"" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */
- )
+ "vendor/version/revision", /* softwareVersion */
+ ),
)
return ids.map { id ->
FingerprintSensorPropertiesInternal(
@@ -58,7 +69,7 @@
5 /* maxEnrollmentsPerUser */,
componentInfo,
sensorType,
- false /* resetLockoutRequiresHardwareAuthToken */
+ false, /* resetLockoutRequiresHardwareAuthToken */
)
}
}
@@ -75,15 +86,15 @@
"vendor/model/revision" /* hardwareVersion */,
"1.01" /* firmwareVersion */,
"00000001" /* serialNumber */,
- "" /* softwareVersion */
+ "", /* softwareVersion */
),
ComponentInfoInternal(
"matchingAlgorithm" /* componentId */,
"" /* hardwareVersion */,
"" /* firmwareVersion */,
"" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */
- )
+ "vendor/version/revision", /* softwareVersion */
+ ),
)
return ids.map { id ->
FaceSensorPropertiesInternal(
@@ -94,7 +105,7 @@
FaceSensorProperties.TYPE_RGB,
true /* supportsFaceDetection */,
true /* supportsSelfIllumination */,
- false /* resetLockoutRequiresHardwareAuthToken */
+ false, /* resetLockoutRequiresHardwareAuthToken */
)
}
}
@@ -145,3 +156,67 @@
info.negativeButtonText = negativeButton
return info
}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal fun TestScope.updateSfpsIndicatorRequests(
+ kosmos: Kosmos,
+ mContext: SysuiTestableContext,
+ primaryBouncerRequest: Boolean? = null,
+ alternateBouncerRequest: Boolean? = null,
+ biometricPromptRequest: Boolean? = null,
+ // TODO(b/365182034): update when rest to unlock feature is implemented
+ // progressBarShowing: Boolean? = null
+) {
+ biometricPromptRequest?.let { hasBiometricPromptRequest ->
+ if (hasBiometricPromptRequest) {
+ kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+ AuthenticationReason.BiometricPromptAuthentication
+ )
+ } else {
+ kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+ AuthenticationReason.NotRunning
+ )
+ }
+ }
+
+ primaryBouncerRequest?.let { hasPrimaryBouncerRequest ->
+ updatePrimaryBouncer(
+ kosmos,
+ mContext,
+ isShowing = hasPrimaryBouncerRequest,
+ isAnimatingAway = false,
+ fpsDetectionRunning = true,
+ isUnlockingWithFpAllowed = true,
+ )
+ }
+
+ alternateBouncerRequest?.let { hasAlternateBouncerRequest ->
+ kosmos.keyguardBouncerRepository.setAlternateVisible(hasAlternateBouncerRequest)
+ }
+
+ // TODO(b/365182034): set progress bar visibility when rest to unlock feature is implemented
+
+ runCurrent()
+}
+
+internal fun updatePrimaryBouncer(
+ kosmos: Kosmos,
+ mContext: SysuiTestableContext,
+ isShowing: Boolean,
+ isAnimatingAway: Boolean,
+ fpsDetectionRunning: Boolean,
+ isUnlockingWithFpAllowed: Boolean,
+) {
+ kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
+ kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
+ val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+ kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
+ primaryStartDisappearAnimation
+ )
+
+ whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
+ .thenReturn(fpsDetectionRunning)
+ whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+ .thenReturn(isUnlockingWithFpAllowed)
+ mContext.orCreateTestableResources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, true)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/model
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/model
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/shared/model/BiometricModalitiesTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 7fa165c..57df662 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -16,64 +16,48 @@
package com.android.systemui.biometrics.ui.binder
-import android.animation.Animator
-import android.graphics.Rect
-import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.display.DisplayManager
-import android.hardware.display.DisplayManagerGlobal
import android.testing.TestableLooper
-import android.view.Display
-import android.view.DisplayInfo
import android.view.LayoutInflater
import android.view.View
-import android.view.ViewPropertyAnimator
-import android.view.WindowInsets
import android.view.WindowManager
-import android.view.WindowMetrics
import android.view.layoutInflater
import android.view.windowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.airbnb.lottie.LottieAnimationView
-import com.android.keyguard.keyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
-import com.android.systemui.biometrics.data.repository.biometricStatusRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
-import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.biometrics.updateSfpsIndicatorRequests
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.displayStateRepository
-import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
-import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.firstValue
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -83,84 +67,25 @@
private val kosmos = testKosmos()
@JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
- @Mock private lateinit var displayManager: DisplayManager
- @Mock
- private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider
@Mock private lateinit var layoutInflater: LayoutInflater
@Mock private lateinit var sideFpsView: View
-
- private val contextDisplayInfo = DisplayInfo()
-
- private var displayWidth: Int = 0
- private var displayHeight: Int = 0
- private var boundsWidth: Int = 0
- private var boundsHeight: Int = 0
-
- private lateinit var deviceConfig: DeviceConfig
- private lateinit var sensorLocation: SensorLocationInternal
-
- enum class DeviceConfig {
- X_ALIGNED,
- Y_ALIGNED,
- }
+ @Captor private lateinit var viewCaptor: ArgumentCaptor<View>
@Before
fun setup() {
allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
-
- mContext = spy(mContext)
-
- val resources = mContext.resources
- whenever(mContext.display)
- .thenReturn(
- Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources)
- )
-
kosmos.layoutInflater = layoutInflater
-
- whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser)
- .thenReturn(MutableStateFlow(false))
-
- context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, kosmos.windowManager)
-
`when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
`when`(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
.thenReturn(mock(LottieAnimationView::class.java))
- with(mock(ViewPropertyAnimator::class.java)) {
- `when`(sideFpsView.animate()).thenReturn(this)
- `when`(alpha(Mockito.anyFloat())).thenReturn(this)
- `when`(setStartDelay(Mockito.anyLong())).thenReturn(this)
- `when`(setDuration(Mockito.anyLong())).thenReturn(this)
- `when`(setListener(any())).thenAnswer {
- (it.arguments[0] as Animator.AnimatorListener).onAnimationEnd(
- mock(Animator::class.java)
- )
- this
- }
- }
}
@Test
fun verifyIndicatorNotAdded_whenInRearDisplayMode() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = true
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
- runCurrent()
-
+ setupTestConfiguration(isInRearDisplayMode = true)
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
verify(kosmos.windowManager, never()).addView(any(), any())
}
}
@@ -168,33 +93,14 @@
@Test
fun verifyIndicatorShowAndHide_onPrimaryBouncerShowAndHide() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- // Show primary bouncer
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
runCurrent()
verify(kosmos.windowManager).addView(any(), any())
// Hide primary bouncer
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = false)
runCurrent()
verify(kosmos.windowManager).removeView(any())
@@ -204,30 +110,19 @@
@Test
fun verifyIndicatorShowAndHide_onAlternateBouncerShowAndHide() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- // Show alternate bouncer
- kosmos.keyguardBouncerRepository.setAlternateVisible(true)
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = true)
runCurrent()
verify(kosmos.windowManager).addView(any(), any())
- var viewCaptor = argumentCaptor<View>()
verify(kosmos.windowManager).addView(viewCaptor.capture(), any())
verify(viewCaptor.firstValue)
.announceForAccessibility(
mContext.getText(R.string.accessibility_side_fingerprint_indicator_label)
)
- // Hide alternate bouncer
- kosmos.keyguardBouncerRepository.setAlternateVisible(false)
+ updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = false)
runCurrent()
verify(kosmos.windowManager).removeView(any())
@@ -237,30 +132,14 @@
@Test
fun verifyIndicatorShownAndHidden_onSystemServerAuthenticationStartedAndStopped() {
kosmos.testScope.runTest {
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
- // System server authentication started
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.BiometricPromptAuthentication
- )
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
runCurrent()
verify(kosmos.windowManager).addView(any(), any())
// System server authentication stopped
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = false)
runCurrent()
verify(kosmos.windowManager).removeView(any())
@@ -269,45 +148,37 @@
// On progress bar shown - hide indicator
// On progress bar hidden - show indicator
+ // TODO(b/365182034): update + enable when rest to unlock feature is implemented
+ @Ignore("b/365182034")
@Test
fun verifyIndicatorProgressBarInteraction() {
kosmos.testScope.runTest {
// Pre-auth conditions
- setupTestConfiguration(
- DeviceConfig.X_ALIGNED,
- rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
- )
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- // Show primary bouncer
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ setupTestConfiguration(isInRearDisplayMode = false)
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
runCurrent()
val inOrder = inOrder(kosmos.windowManager)
-
// Verify indicator shown
inOrder.verify(kosmos.windowManager).addView(any(), any())
// Set progress bar visible
- kosmos.sideFpsProgressBarViewModel.setVisible(true)
-
+ updateSfpsIndicatorRequests(
+ kosmos,
+ mContext,
+ primaryBouncerRequest = true,
+ ) // , progressBarShowing = true)
runCurrent()
// Verify indicator hidden
inOrder.verify(kosmos.windowManager).removeView(any())
// Set progress bar invisible
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
+ updateSfpsIndicatorRequests(
+ kosmos,
+ mContext,
+ primaryBouncerRequest = true,
+ ) // , progressBarShowing = false)
runCurrent()
// Verify indicator shown
@@ -315,78 +186,18 @@
}
}
- private fun updatePrimaryBouncer(
- isShowing: Boolean,
- isAnimatingAway: Boolean,
- fpsDetectionRunning: Boolean,
- isUnlockingWithFpAllowed: Boolean,
- ) {
- kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
- kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
- val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
- kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
- primaryStartDisappearAnimation
- )
-
- whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
- .thenReturn(fpsDetectionRunning)
- whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
- .thenReturn(isUnlockingWithFpAllowed)
- mContext.orCreateTestableResources.addOverride(
- R.bool.config_show_sidefps_hint_on_bouncer,
- true
- )
- }
-
- private suspend fun TestScope.setupTestConfiguration(
- deviceConfig: DeviceConfig,
- rotation: DisplayRotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode: Boolean,
- ) {
- this@SideFpsOverlayViewBinderTest.deviceConfig = deviceConfig
-
- when (deviceConfig) {
- DeviceConfig.X_ALIGNED -> {
- displayWidth = 3000
- displayHeight = 1500
- boundsWidth = 200
- boundsHeight = 100
- sensorLocation = SensorLocationInternal("", 2500, 0, boundsWidth / 2)
- }
- DeviceConfig.Y_ALIGNED -> {
- displayWidth = 2500
- displayHeight = 2000
- boundsWidth = 100
- boundsHeight = 200
- sensorLocation = SensorLocationInternal("", displayWidth, 300, boundsHeight / 2)
- }
- }
-
- whenever(kosmos.windowManager.maximumWindowMetrics)
- .thenReturn(
- WindowMetrics(
- Rect(0, 0, displayWidth, displayHeight),
- mock(WindowInsets::class.java),
- )
- )
-
- contextDisplayInfo.uniqueId = DISPLAY_ID
-
+ private suspend fun TestScope.setupTestConfiguration(isInRearDisplayMode: Boolean) {
kosmos.fingerprintPropertyRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.POWER_BUTTON,
- sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
+ sensorLocations = emptyMap(),
)
kosmos.displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
- kosmos.displayStateRepository.setCurrentRotation(rotation)
+ kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
kosmos.displayRepository.emitDisplayChangeEvent(0)
kosmos.sideFpsOverlayViewBinder.start()
runCurrent()
}
-
- companion object {
- private const val DISPLAY_ID = "displayId"
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 0db7b62..84d062a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -30,23 +30,19 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.airbnb.lottie.model.KeyPath
-import com.android.keyguard.keyguardUpdateMonitor
import com.android.settingslib.Utils
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
-import com.android.systemui.biometrics.data.repository.biometricStatusRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
-import com.android.systemui.biometrics.shared.model.AuthenticationReason
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.LottieCallback
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.biometrics.updateSfpsIndicatorRequests
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.display.data.repository.displayStateRepository
-import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
@@ -84,17 +80,17 @@
private val indicatorColor =
Utils.getColorAttrDefaultColor(
context,
- com.android.internal.R.attr.materialColorPrimaryFixed
+ com.android.internal.R.attr.materialColorPrimaryFixed,
)
private val outerRimColor =
Utils.getColorAttrDefaultColor(
context,
- com.android.internal.R.attr.materialColorPrimaryFixedDim
+ com.android.internal.R.attr.materialColorPrimaryFixedDim,
)
private val chevronFill =
Utils.getColorAttrDefaultColor(
context,
- com.android.internal.R.attr.materialColorOnPrimaryFixed
+ com.android.internal.R.attr.materialColorOnPrimaryFixed,
)
private val color_blue400 =
context.getColor(com.android.settingslib.color.R.color.settingslib_color_blue400)
@@ -133,7 +129,7 @@
setupTestConfiguration(
DeviceConfig.X_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewProperties by
@@ -167,7 +163,7 @@
setupTestConfiguration(
DeviceConfig.Y_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewProperties by
@@ -201,7 +197,7 @@
setupTestConfiguration(
DeviceConfig.X_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewParams by
@@ -243,7 +239,7 @@
setupTestConfiguration(
DeviceConfig.Y_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
- isInRearDisplayMode = false
+ isInRearDisplayMode = false,
)
val overlayViewParams by
@@ -284,17 +280,7 @@
kosmos.testScope.runTest {
val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.NotRunning
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- updatePrimaryBouncer(
- isShowing = true,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
runCurrent()
assertThat(lottieCallbacks)
@@ -312,17 +298,7 @@
val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
setDarkMode(true)
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.BiometricPromptAuthentication
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
runCurrent()
assertThat(lottieCallbacks)
@@ -338,17 +314,7 @@
val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
setDarkMode(false)
- kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
- AuthenticationReason.BiometricPromptAuthentication
- )
- kosmos.sideFpsProgressBarViewModel.setVisible(false)
-
- updatePrimaryBouncer(
- isShowing = false,
- isAnimatingAway = false,
- fpsDetectionRunning = true,
- isUnlockingWithFpAllowed = true
- )
+ updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
runCurrent()
assertThat(lottieCallbacks)
@@ -371,29 +337,6 @@
mContext.resources.configuration.uiMode = uiMode
}
- private fun updatePrimaryBouncer(
- isShowing: Boolean,
- isAnimatingAway: Boolean,
- fpsDetectionRunning: Boolean,
- isUnlockingWithFpAllowed: Boolean,
- ) {
- kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
- kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
- val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
- kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
- primaryStartDisappearAnimation
- )
-
- whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
- .thenReturn(fpsDetectionRunning)
- whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
- .thenReturn(isUnlockingWithFpAllowed)
- mContext.orCreateTestableResources.addOverride(
- R.bool.config_show_sidefps_hint_on_bouncer,
- true
- )
- }
-
private suspend fun TestScope.setupTestConfiguration(
deviceConfig: DeviceConfig,
rotation: DisplayRotation = DisplayRotation.ROTATION_0,
@@ -432,7 +375,7 @@
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.POWER_BUTTON,
- sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
+ sensorLocations = mapOf(DISPLAY_ID to sensorLocation),
)
kosmos.displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/BrightLineClassifierTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/ZigZagClassifierTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/classifier/ZigZagClassifierTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/classifier/ZigZagClassifierTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index d4d966a..2312bbd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -22,6 +22,7 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.nano.CommunalHubState
+import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.lifecycle.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
@@ -102,7 +103,7 @@
widgetId = widgetId,
provider = provider,
rank = rank,
- userSerialNumber = userSerialNumber
+ userSerialNumber = userSerialNumber,
)
}
assertThat(widgets())
@@ -110,7 +111,7 @@
communalItemRankEntry1,
communalWidgetItemEntry1,
communalItemRankEntry2,
- communalWidgetItemEntry2
+ communalWidgetItemEntry2,
)
}
@@ -129,7 +130,7 @@
communalWidgetDao.addWidget(
widgetId = widgetId,
provider = provider,
- userSerialNumber = userSerialNumber
+ userSerialNumber = userSerialNumber,
)
}
@@ -165,7 +166,7 @@
communalItemRankEntry1,
communalWidgetItemEntry1,
communalItemRankEntry2,
- communalWidgetItemEntry2
+ communalWidgetItemEntry2,
)
communalWidgetDao.deleteWidgetById(communalWidgetItemEntry1.widgetId)
@@ -251,6 +252,7 @@
componentName = "pk_name/cls_name_4",
itemId = 4L,
userSerialNumber = 0,
+ spanY = 3,
)
assertThat(widgets())
.containsExactly(
@@ -267,6 +269,68 @@
}
@Test
+ fun addWidget_withDifferentSpanY_readsCorrectValuesInDb() =
+ testScope.runTest {
+ val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+ // Add widgets with different spanY values
+ communalWidgetDao.addWidget(
+ widgetId = 1,
+ provider = ComponentName("pkg_name", "cls_name_1"),
+ rank = 0,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.FULL.span,
+ )
+ communalWidgetDao.addWidget(
+ widgetId = 2,
+ provider = ComponentName("pkg_name", "cls_name_2"),
+ rank = 1,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.HALF.span,
+ )
+ communalWidgetDao.addWidget(
+ widgetId = 3,
+ provider = ComponentName("pkg_name", "cls_name_3"),
+ rank = 2,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.THIRD.span,
+ )
+
+ // Verify that the widgets have the correct spanY values
+ assertThat(widgets())
+ .containsExactly(
+ CommunalItemRank(uid = 1L, rank = 0),
+ CommunalWidgetItem(
+ uid = 1L,
+ widgetId = 1,
+ componentName = "pkg_name/cls_name_1",
+ itemId = 1L,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.FULL.span,
+ ),
+ CommunalItemRank(uid = 2L, rank = 1),
+ CommunalWidgetItem(
+ uid = 2L,
+ widgetId = 2,
+ componentName = "pkg_name/cls_name_2",
+ itemId = 2L,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.HALF.span,
+ ),
+ CommunalItemRank(uid = 3L, rank = 2),
+ CommunalWidgetItem(
+ uid = 3L,
+ widgetId = 3,
+ componentName = "pkg_name/cls_name_3",
+ itemId = 3L,
+ userSerialNumber = 0,
+ spanY = CommunalContentSize.THIRD.span,
+ ),
+ )
+ .inOrder()
+ }
+
+ @Test
fun restoreCommunalHubState() =
testScope.runTest {
// Set up db
@@ -288,6 +352,7 @@
componentName = fakeWidget.componentName,
itemId = rank.uid,
userSerialNumber = fakeWidget.userSerialNumber,
+ spanY = 3,
)
expected[rank] = widget
}
@@ -343,6 +408,7 @@
componentName = widgetInfo1.provider.flattenToString(),
itemId = communalItemRankEntry1.uid,
userSerialNumber = widgetInfo1.userSerialNumber,
+ spanY = 3,
)
val communalWidgetItemEntry2 =
CommunalWidgetItem(
@@ -351,6 +417,7 @@
componentName = widgetInfo2.provider.flattenToString(),
itemId = communalItemRankEntry2.uid,
userSerialNumber = widgetInfo2.userSerialNumber,
+ spanY = 3,
)
val communalWidgetItemEntry3 =
CommunalWidgetItem(
@@ -359,6 +426,7 @@
componentName = widgetInfo3.provider.flattenToString(),
itemId = communalItemRankEntry3.uid,
userSerialNumber = widgetInfo3.userSerialNumber,
+ spanY = 3,
)
val fakeState =
CommunalHubState().apply {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
index eba395b..596db07 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
@@ -117,6 +117,7 @@
componentName = defaultWidgets[0],
rank = 0,
userSerialNumber = 0,
+ spanY = 3,
)
verify(communalWidgetDao)
.addWidget(
@@ -124,6 +125,7 @@
componentName = defaultWidgets[1],
rank = 1,
userSerialNumber = 0,
+ spanY = 3,
)
verify(communalWidgetDao)
.addWidget(
@@ -131,6 +133,7 @@
componentName = defaultWidgets[2],
rank = 2,
userSerialNumber = 0,
+ spanY = 3,
)
}
@@ -152,6 +155,7 @@
componentName = any(),
rank = anyInt(),
userSerialNumber = anyInt(),
+ spanY = anyInt(),
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 980a5ec..3d30ecc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -143,7 +143,8 @@
fun communalWidgets_queryWidgetsFromDb() =
testScope.runTest {
val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
- val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0)
+ val communalWidgetItemEntry =
+ CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L, 0, 3)
fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
fakeProviders.value = mapOf(1 to providerInfoA)
@@ -169,19 +170,15 @@
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3),
CommunalItemRank(uid = 2L, rank = 2) to
- CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3),
CommunalItemRank(uid = 3L, rank = 3) to
- CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0),
+ CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L, 0, 3),
CommunalItemRank(uid = 4L, rank = 4) to
- CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0),
+ CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L, 0, 3),
)
- fakeProviders.value =
- mapOf(
- 1 to providerInfoA,
- 2 to providerInfoB,
- )
+ fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB)
// Expect to see only widget 1 and 2
val communalWidgets by collectLastValue(underTest.communalWidgets)
@@ -207,15 +204,11 @@
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3),
CommunalItemRank(uid = 2L, rank = 2) to
- CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3),
)
- fakeProviders.value =
- mapOf(
- 1 to providerInfoA,
- 2 to providerInfoB,
- )
+ fakeProviders.value = mapOf(1 to providerInfoA, 2 to providerInfoB)
// Expect two widgets
val communalWidgets by collectLastValue(underTest.communalWidgets)
@@ -235,11 +228,7 @@
)
// Provider info updated for widget 1
- fakeProviders.value =
- mapOf(
- 1 to providerInfoC,
- 2 to providerInfoB,
- )
+ fakeProviders.value = mapOf(1 to providerInfoC, 2 to providerInfoB)
runCurrent()
assertThat(communalWidgets)
@@ -269,7 +258,7 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -294,7 +283,7 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -303,7 +292,7 @@
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
verify(communalWidgetDao, never())
- .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt())
+ .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt(), anyInt())
verify(appWidgetHost).deleteAppWidgetId(id)
// Verify backup not requested
@@ -321,7 +310,7 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -332,7 +321,7 @@
verify(communalWidgetHost).allocateIdAndBindWidget(provider, mainUser)
verify(communalWidgetDao, never())
- .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt())
+ .addWidget(anyInt(), any<ComponentName>(), anyInt(), anyInt(), anyInt())
verify(appWidgetHost).deleteAppWidgetId(id)
// Verify backup not requested
@@ -350,7 +339,7 @@
whenever(
communalWidgetHost.allocateIdAndBindWidget(
any<ComponentName>(),
- any<UserHandle>()
+ any<UserHandle>(),
)
)
.thenReturn(id)
@@ -650,8 +639,10 @@
eq(newWidgetId),
componentNameCaptor.capture(),
eq(2),
- eq(testUserSerialNumber(workProfile))
+ eq(testUserSerialNumber(workProfile)),
+ anyInt(),
)
+
assertThat(componentNameCaptor.firstValue)
.isEqualTo(ComponentName("pk_name", "fake_widget_2"))
}
@@ -662,9 +653,9 @@
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3),
CommunalItemRank(uid = 2L, rank = 2) to
- CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0),
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L, 0, 3),
)
// Widget 1 is installed
@@ -707,7 +698,7 @@
fakeWidgets.value =
mapOf(
CommunalItemRank(uid = 1L, rank = 1) to
- CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0),
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L, 0, 3)
)
// Widget 1 is pending install
@@ -732,7 +723,7 @@
componentName = ComponentName("pk_1", "cls_1"),
icon = fakeIcon,
user = mainUser,
- ),
+ )
)
// Package for widget 1 finished installing
@@ -749,10 +740,23 @@
appWidgetId = 1,
providerInfo = providerInfoA,
rank = 1,
- ),
+ )
)
}
+ @Test
+ fun updateWidgetSpanY_updatesWidgetInDaoAndRequestsBackup() =
+ testScope.runTest {
+ val widgetId = 1
+ val newSpanY = 6
+
+ underTest.updateWidgetSpanY(widgetId, newSpanY)
+ runCurrent()
+
+ verify(communalWidgetDao).updateWidgetSpanY(widgetId, newSpanY)
+ verify(backupManager).dataChanged()
+ }
+
private fun setAppWidgetIds(ids: List<Int>) {
whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch
diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 8f9e238..8b13411 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -145,7 +145,7 @@
fakeInputManager.addPhysicalKeyboard(
PHYSICAL_NOT_FULL_KEYBOARD_ID,
- isFullKeyboard = false
+ isFullKeyboard = false,
)
assertThat(isKeyboardConnected).isFalse()
@@ -223,7 +223,7 @@
backlightListenerCaptor.value.onBacklightChanged(
current = 1,
max = 5,
- triggeredByKeyPress = false
+ triggeredByKeyPress = false,
)
assertThat(backlight).isNull()
}
@@ -239,7 +239,7 @@
backlightListenerCaptor.value.onBacklightChanged(
current = 1,
max = 5,
- triggeredByKeyPress = true
+ triggeredByKeyPress = true,
)
assertThat(backlight).isNotNull()
}
@@ -318,15 +318,75 @@
}
}
+ @Test
+ fun connectedKeyboards_emitsAllKeyboards() {
+ testScope.runTest {
+ val firstKeyboard = Keyboard(vendorId = 1, productId = 1)
+ val secondKeyboard = Keyboard(vendorId = 2, productId = 2)
+ captureDeviceListener()
+ val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+ fakeInputManager.addPhysicalKeyboard(
+ PHYSICAL_FULL_KEYBOARD_ID,
+ vendorId = firstKeyboard.vendorId,
+ productId = firstKeyboard.productId,
+ )
+ assertThat(keyboards)
+ .containsExactly(Keyboard(firstKeyboard.vendorId, firstKeyboard.productId))
+
+ fakeInputManager.addPhysicalKeyboard(
+ ANOTHER_PHYSICAL_FULL_KEYBOARD_ID,
+ vendorId = secondKeyboard.vendorId,
+ productId = secondKeyboard.productId,
+ )
+ assertThat(keyboards)
+ .containsExactly(
+ Keyboard(firstKeyboard.vendorId, firstKeyboard.productId),
+ Keyboard(secondKeyboard.vendorId, secondKeyboard.productId),
+ )
+ }
+ }
+
+ @Test
+ fun connectedKeyboards_emitsOnlyFullPhysicalKeyboards() {
+ testScope.runTest {
+ captureDeviceListener()
+ val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addDevice(VIRTUAL_FULL_KEYBOARD_ID, SOURCE_KEYBOARD)
+ fakeInputManager.addPhysicalKeyboard(
+ PHYSICAL_NOT_FULL_KEYBOARD_ID,
+ isFullKeyboard = false,
+ )
+
+ assertThat(keyboards).hasSize(1)
+ }
+ }
+
+ @Test
+ fun connectedKeyboards_emitsOnlyConnectedKeyboards() {
+ testScope.runTest {
+ captureDeviceListener()
+ val keyboards by collectLastValueImmediately(underTest.connectedKeyboards)
+
+ fakeInputManager.addPhysicalKeyboard(PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.addPhysicalKeyboard(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+ fakeInputManager.removeDevice(ANOTHER_PHYSICAL_FULL_KEYBOARD_ID)
+
+ assertThat(keyboards).hasSize(1)
+ }
+ }
+
private fun KeyboardBacklightListener.onBacklightChanged(
current: Int,
max: Int,
- triggeredByKeyPress: Boolean = true
+ triggeredByKeyPress: Boolean = true,
) {
onKeyboardBacklightChanged(
/* deviceId= */ 0,
TestBacklightState(current, max),
- triggeredByKeyPress
+ triggeredByKeyPress,
)
}
@@ -343,7 +403,7 @@
private class TestBacklightState(
private val brightnessLevel: Int,
- private val maxBrightnessLevel: Int
+ private val maxBrightnessLevel: Int,
) : KeyboardBacklightState() {
override fun getBrightnessLevel() = brightnessLevel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index 073ed61..b6ec6a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -21,16 +21,23 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.fakeLightRevealScrimRepository
+import com.android.systemui.keyguard.data.repository.DEFAULT_REVEAL_DURATION
import com.android.systemui.keyguard.data.repository.DEFAULT_REVEAL_EFFECT
import com.android.systemui.keyguard.data.repository.FakeLightRevealScrimRepository
+import com.android.systemui.keyguard.data.repository.FakeLightRevealScrimRepository.RevealAnimatorRequest
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -52,6 +59,8 @@
private val fakeKeyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val fakePowerRepository = kosmos.fakePowerRepository
+
private val underTest = kosmos.lightRevealScrimInteractor
private val reveal1 =
@@ -107,4 +116,50 @@
runCurrent()
assertEquals(listOf(DEFAULT_REVEAL_EFFECT, reveal1, reveal2), values)
}
+
+ @Test
+ fun transitionToAod_folding_doesNotAnimateTheScrim() =
+ kosmos.testScope.runTest {
+ updateWakefulness(goToSleepReason = WakeSleepReason.FOLD)
+ runCurrent()
+
+ // Transition to AOD
+ fakeKeyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED)
+ )
+ runCurrent()
+
+ assertThat(fakeLightRevealScrimRepository.revealAnimatorRequests.last())
+ .isEqualTo(RevealAnimatorRequest(reveal = false, duration = 0))
+ }
+
+ @Test
+ fun transitionToAod_powerButton_animatesTheScrim() =
+ kosmos.testScope.runTest {
+ updateWakefulness(goToSleepReason = WakeSleepReason.POWER_BUTTON)
+ runCurrent()
+
+ // Transition to AOD
+ fakeKeyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED)
+ )
+ runCurrent()
+
+ assertThat(fakeLightRevealScrimRepository.revealAnimatorRequests.last())
+ .isEqualTo(
+ RevealAnimatorRequest(
+ reveal = false,
+ duration = DEFAULT_REVEAL_DURATION
+ )
+ )
+ }
+
+ private fun updateWakefulness(goToSleepReason: WakeSleepReason) {
+ fakePowerRepository.updateWakefulness(
+ rawState = WakefulnessState.STARTING_TO_SLEEP,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = goToSleepReason,
+ powerButtonLaunchGestureTriggered = false
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/log/table/TableLogBufferTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/log/table/TableLogBufferTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
index 7da2e9a..fc9e595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
@@ -408,6 +408,40 @@
verify(mockImageLoader, times(1)).loadBitmap(any(), anyInt(), anyInt(), anyInt())
}
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testLoadMediaDataInBg_fromResumeToActive_doesNotCancelResumeToActiveTask() =
+ testScope.runTest {
+ val mockImageLoader = mock<ImageLoader>()
+ val mediaDataLoader =
+ MediaDataLoader(
+ context,
+ testDispatcher,
+ testScope,
+ mediaControllerFactory,
+ mediaFlags,
+ mockImageLoader,
+ statusBarManager,
+ )
+ metadataBuilder.putString(
+ MediaMetadata.METADATA_KEY_ALBUM_ART_URI,
+ "content://album_art_uri",
+ )
+
+ testScope.launch {
+ mediaDataLoader.loadMediaData(
+ KEY,
+ createMediaNotification(),
+ isConvertingToActive = true,
+ )
+ }
+ testScope.launch { mediaDataLoader.loadMediaData(KEY, createMediaNotification()) }
+ testScope.launch { mediaDataLoader.loadMediaData(KEY, createMediaNotification()) }
+ testScope.advanceUntilIdle()
+
+ verify(mockImageLoader, times(2)).loadBitmap(any(), anyInt(), anyInt(), anyInt())
+ }
+
private fun createMediaNotification(
mediaSession: MediaSession? = session,
applicationInfo: ApplicationInfo? = null,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
similarity index 96%
rename from packages/SystemUI/tests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
index 84ec1a5..77be8c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.mediarouter.data.repository
import androidx.test.filters.SmallTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -28,9 +29,11 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
class MediaRouterRepositoryTest : SysuiTestCase() {
val kosmos = Kosmos()
val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
index acd69af..da16640 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt
@@ -26,10 +26,11 @@
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@RunWithLooper
class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) :
AbstractQSFragmentComposeViewModelTest() {
@@ -75,7 +76,7 @@
companion object {
private const val EXPANSION = 0.3f
- @Parameterized.Parameters(name = "{0}")
+ @Parameters(name = "{0}")
@JvmStatic
fun createTestData(): List<TestData> {
return statusBarStates.flatMap { statusBarState ->
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
index 664315d..ca9500b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerHapticsViewModelTest.kt
@@ -51,7 +51,7 @@
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
-import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.verifyNoMoreInteractions
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -134,7 +134,7 @@
runCurrent()
// THEN the view does not play a haptic feedback constant
- verifyZeroInteractions(view)
+ verifyNoMoreInteractions(view)
}
@EnableFlags(Flags.FLAG_MSDL_FEEDBACK, Flags.FLAG_DUAL_SHADE)
@@ -202,7 +202,7 @@
runCurrent()
// THEN the view does not play a haptic feedback constant
- verifyZeroInteractions(view)
+ verifyNoMoreInteractions(view)
}
private fun createTransitionState(from: SceneKey, to: ContentKey) =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
index 15d6881..fcb366b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModelTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
@@ -50,6 +51,7 @@
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
@@ -248,6 +250,27 @@
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Communal))
}
+ @Test
+ fun upTransitionSceneKey_neverGoesBackToShadeScene() =
+ testScope.runTest {
+ val actions by collectValues(underTest.actions)
+ val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ kosmos.sceneInteractor.changeScene(Scenes.Shade, "")
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+
+ kosmos.sceneInteractor.changeScene(Scenes.QuickSettings, "")
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+
+ actions.forEachIndexed { index, map ->
+ assertWithMessage(
+ "Actions on index $index is incorrectly mapping back to the Shade scene!"
+ )
+ .that((map[Swipe.Up] as? UserActionResult.ChangeScene)?.toScene)
+ .isNotEqualTo(Scenes.Shade)
+ }
+ }
+
private fun TestScope.setDeviceEntered(isEntered: Boolean) {
if (isEntered) {
// Unlock the device marking the device has entered.
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 523a89a..5b0b59d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -71,7 +71,11 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
+import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.DisableSceneContainer;
+import com.android.systemui.flags.EnableSceneContainer;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
import com.android.systemui.log.LogWtfHandlerRule;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -90,6 +94,10 @@
import com.google.android.collect.Lists;
+import dagger.Lazy;
+
+import kotlinx.coroutines.flow.StateFlow;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -152,6 +160,12 @@
private BroadcastDispatcher mBroadcastDispatcher;
@Mock
private KeyguardStateController mKeyguardStateController;
+ @Mock
+ private Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+ @Mock
+ private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
+ @Mock
+ private StateFlow<DeviceUnlockStatus> mDeviceUnlockStatusStateFlow;
private UserInfo mCurrentUser;
private UserInfo mSecondaryUser;
@@ -238,6 +252,9 @@
mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
mLockscreenUserManager.setUpWithPresenter(mPresenter);
+ when(mDeviceUnlockedInteractor.getDeviceUnlockStatus())
+ .thenReturn(mDeviceUnlockStatusStateFlow);
+
mBackgroundExecutor.runAllReady();
}
@@ -493,7 +510,8 @@
}
@Test
- public void testUpdateIsPublicMode() {
+ @DisableSceneContainer
+ public void testUpdateIsPublicMode_sceneContainerDisabled() {
when(mKeyguardStateController.isMethodSecure()).thenReturn(true);
when(mKeyguardStateController.isShowing()).thenReturn(false);
@@ -527,6 +545,57 @@
mBackgroundExecutor.runAllReady();
assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
verify(listener, never()).onNotificationStateChanged();
+
+ verify(mDeviceUnlockedInteractorLazy, never()).get();
+ }
+
+ @Test
+ @EnableSceneContainer
+ public void testUpdateIsPublicMode_sceneContainerEnabled() {
+ when(mDeviceUnlockedInteractorLazy.get()).thenReturn(mDeviceUnlockedInteractor);
+
+ // device is unlocked
+ when(mDeviceUnlockStatusStateFlow.getValue()).thenReturn(new DeviceUnlockStatus(
+ /* isUnlocked = */ true,
+ /* deviceUnlockSource = */ null
+ ));
+
+ NotificationStateChangedListener listener = mock(NotificationStateChangedListener.class);
+ mLockscreenUserManager.addNotificationStateChangedListener(listener);
+ mLockscreenUserManager.mCurrentProfiles.append(0, mock(UserInfo.class));
+
+ // first call explicitly sets user 0 to not public; notifies
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener).onNotificationStateChanged();
+ clearInvocations(listener);
+
+ // calling again has no changes; does not notify
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertFalse(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener, never()).onNotificationStateChanged();
+
+ // device is not unlocked
+ when(mDeviceUnlockStatusStateFlow.getValue()).thenReturn(new DeviceUnlockStatus(
+ /* isUnlocked = */ false,
+ /* deviceUnlockSource = */ null
+ ));
+
+ // Calling again with device now not unlocked makes user 0 public; notifies
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener).onNotificationStateChanged();
+ clearInvocations(listener);
+
+ // calling again has no changes; does not notify
+ mLockscreenUserManager.updatePublicMode();
+ mBackgroundExecutor.runAllReady();
+ assertTrue(mLockscreenUserManager.isLockscreenPublicMode(0));
+ verify(listener, never()).onNotificationStateChanged();
}
@Test
@@ -972,7 +1041,9 @@
mSettings,
mock(DumpManager.class),
mock(LockPatternUtils.class),
- mFakeFeatureFlags);
+ mFakeFeatureFlags,
+ mDeviceUnlockedInteractorLazy
+ );
}
public BroadcastReceiver getBaseBroadcastReceiverForTest() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
index 28857a0..34f4608 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -208,8 +208,8 @@
assertThat(footerVisible).isTrue()
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenHistoryDisabled_leadsToSettingsPage() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -222,8 +222,8 @@
assertThat(onClick?.backStack).isEmpty()
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenHistoryEnabled_leadsToHistoryPage() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -237,8 +237,8 @@
.containsExactly(Settings.ACTION_NOTIFICATION_SETTINGS)
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenOneModeHidingNotifications_leadsToModeSettings() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
@@ -263,8 +263,8 @@
.containsExactly(Settings.ACTION_ZEN_MODE_SETTINGS)
}
- @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
@Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
fun onClick_whenMultipleModesHidingNotifications_leadsToGeneralModesSettings() =
testScope.runTest {
val onClick by collectLastValue(underTest.onClick)
diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 3b3ed39..91cd019 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -215,17 +215,4 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
tools:srcCompat="@tools:sample/avatars" />
-
- <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
- android:id="@+id/biometric_icon_overlay"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_gravity="center"
- android:contentDescription="@null"
- android:scaleType="fitXY"
- android:importantForAccessibility="no"
- app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
- app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
- app:layout_constraintStart_toStartOf="@+id/biometric_icon"
- app:layout_constraintTop_toTopOf="@+id/biometric_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
index 2a00495..51117a7 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
@@ -40,19 +40,6 @@
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
- <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
- android:id="@+id/biometric_icon_overlay"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:layout_gravity="center"
- android:contentDescription="@null"
- android:scaleType="fitXY"
- android:importantForAccessibility="no"
- app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
- app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
- app:layout_constraintStart_toStartOf="@+id/biometric_icon"
- app:layout_constraintTop_toTopOf="@+id/biometric_icon" />
-
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml
index 9b7ae5d..cea405b 100644
--- a/packages/SystemUI/res/values-en-rGB/strings.xml
+++ b/packages/SystemUI/res/values-en-rGB/strings.xml
@@ -573,8 +573,7 @@
<string name="notification_section_header_conversations" msgid="821834744538345661">"Conversations"</string>
<string name="accessibility_notification_section_header_gentle_clear_all" msgid="6490207897764933919">"Clear all silent notifications"</string>
<string name="dnd_suppressing_shade_text" msgid="5588252250634464042">"Notifications paused by Do Not Disturb"</string>
- <!-- no translation found for modes_suppressing_shade_text (6037581130837903239) -->
- <skip />
+ <string name="modes_suppressing_shade_text" msgid="6037581130837903239">"{count,plural,offset:1 =0{No notifications}=1{Notifications paused by {mode}}=2{Notifications paused by {mode} and one other mode}other{Notifications paused by {mode} and # other modes}}"</string>
<string name="media_projection_action_text" msgid="3634906766918186440">"Start now"</string>
<string name="empty_shade_text" msgid="8935967157319717412">"No notifications"</string>
<string name="no_unseen_notif_text" msgid="395512586119868682">"No new notifications"</string>
@@ -1436,6 +1435,8 @@
<string name="all_apps_edu_toast_content" msgid="8807496014667211562">"To view all your apps, press the action key on your keyboard"</string>
<string name="redacted_notification_single_line_title" msgid="212019960919261670">"Redacted"</string>
<string name="redacted_notification_single_line_text" msgid="8684166405005242945">"Unlock to view"</string>
+ <!-- no translation found for contextual_education_dialog_title (4630392552837487324) -->
+ <skip />
<string name="back_edu_notification_title" msgid="5624780717751357278">"Use your touchpad to go back"</string>
<string name="back_edu_notification_content" msgid="2497557451540954068">"Swipe left or right using three fingers. Tap to learn more gestures."</string>
<string name="home_edu_notification_title" msgid="6097902076909654045">"Use your touchpad to go home"</string>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index e9dd039f3..7bd4ca8 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -64,4 +64,9 @@
<item name="android:windowLightNavigationBar">false</item>
</style>
+ <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+ <!-- To make the dialog wrap to content when the education text is short -->
+ <item name="windowMinWidthMajor">0%</item>
+ <item name="windowMinWidthMinor">0%</item>
+ </style>
</resources>
diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml
index 5c9679d..399523e 100644
--- a/packages/SystemUI/res/values-pt-rBR/strings.xml
+++ b/packages/SystemUI/res/values-pt-rBR/strings.xml
@@ -1436,6 +1436,8 @@
<string name="all_apps_edu_toast_content" msgid="8807496014667211562">"Para ver todos os apps, pressione a tecla de ação no teclado"</string>
<string name="redacted_notification_single_line_title" msgid="212019960919261670">"Encoberto"</string>
<string name="redacted_notification_single_line_text" msgid="8684166405005242945">"Desbloquear para visualizar"</string>
+ <!-- no translation found for contextual_education_dialog_title (4630392552837487324) -->
+ <skip />
<string name="back_edu_notification_title" msgid="5624780717751357278">"Use o touchpad para voltar"</string>
<string name="back_edu_notification_content" msgid="2497557451540954068">"Deslize para a esquerda ou direita usando três dedos. Toque para aprender outros gestos."</string>
<string name="home_edu_notification_title" msgid="6097902076909654045">"Use o touchpad para acessar a tela inicial"</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b34d6e4..1c09f84 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1721,7 +1721,7 @@
<item name="android:windowLightNavigationBar">true</item>
</style>
- <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Dialog.NoActionBar">
+ <style name="ContextualEduDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar">
<!-- To make the dialog wrap to content when the education text is short -->
<item name="windowMinWidthMajor">0%</item>
<item name="windowMinWidthMinor">0%</item>
diff --git a/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json
new file mode 100644
index 0000000..c3fb8d4
--- /dev/null
+++ b/packages/SystemUI/schemas/com.android.systemui.communal.data.db.CommunalDatabase/4.json
@@ -0,0 +1,88 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 4,
+ "identityHash": "a49f2f7d25cf12d1baf9a3a3e6243b64",
+ "entities": [
+ {
+ "tableName": "communal_widget_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `widget_id` INTEGER NOT NULL, `component_name` TEXT NOT NULL, `item_id` INTEGER NOT NULL, `user_serial_number` INTEGER NOT NULL DEFAULT -1, `span_y` INTEGER NOT NULL DEFAULT 3)",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "widgetId",
+ "columnName": "widget_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "componentName",
+ "columnName": "component_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "itemId",
+ "columnName": "item_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userSerialNumber",
+ "columnName": "user_serial_number",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "spanY",
+ "columnName": "span_y",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "3"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ },
+ {
+ "tableName": "communal_item_rank_table",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `rank` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rank",
+ "columnName": "rank",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "uid"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a49f2f7d25cf12d1baf9a3a3e6243b64')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 22130f8..8e01e04 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -114,6 +114,7 @@
import com.android.internal.util.LatencyTracker;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
+import com.android.keyguard.logging.SimLogger;
import com.android.settingslib.Utils;
import com.android.settingslib.WirelessUtils;
import com.android.settingslib.fuelgauge.BatteryStatus;
@@ -285,6 +286,7 @@
private final Context mContext;
private final UserTracker mUserTracker;
private final KeyguardUpdateMonitorLogger mLogger;
+ private final SimLogger mSimLogger;
private final boolean mIsSystemUser;
private final Provider<JavaAdapter> mJavaAdapter;
private final Provider<SceneInteractor> mSceneInteractor;
@@ -582,14 +584,14 @@
private void handleSimSubscriptionInfoChanged() {
Assert.isMainThread();
- mLogger.v("onSubscriptionInfoChanged()");
+ mSimLogger.v("onSubscriptionInfoChanged()");
List<SubscriptionInfo> subscriptionInfos = getSubscriptionInfo(true /* forceReload */);
if (!subscriptionInfos.isEmpty()) {
for (SubscriptionInfo subInfo : subscriptionInfos) {
- mLogger.logSubInfo(subInfo);
+ mSimLogger.logSubInfo(subInfo);
}
} else {
- mLogger.v("onSubscriptionInfoChanged: list is null");
+ mSimLogger.v("onSubscriptionInfoChanged: list is null");
}
// Hack level over 9000: Because the subscription id is not yet valid when we see the
@@ -612,7 +614,7 @@
while (iter.hasNext()) {
Map.Entry<Integer, SimData> simData = iter.next();
if (!activeSubIds.contains(simData.getKey())) {
- mLogger.logInvalidSubId(simData.getKey());
+ mSimLogger.logInvalidSubId(simData.getKey());
iter.remove();
SimData data = simData.getValue();
@@ -1700,7 +1702,7 @@
}
return;
}
- mLogger.logSimStateFromIntent(action,
+ mSimLogger.logSimStateFromIntent(action,
intent.getStringExtra(Intent.EXTRA_SIM_STATE),
args.slotId,
args.subId);
@@ -1720,7 +1722,7 @@
ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
- mLogger.logServiceStateIntent(action, serviceState, subId);
+ mSimLogger.logServiceStateIntent(action, serviceState, subId);
mHandler.sendMessage(
mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState));
} else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED.equals(action)) {
@@ -2154,6 +2156,7 @@
LatencyTracker latencyTracker,
ActiveUnlockConfig activeUnlockConfiguration,
KeyguardUpdateMonitorLogger logger,
+ SimLogger simLogger,
UiEventLogger uiEventLogger,
// This has to be a provider because SessionTracker depends on KeyguardUpdateMonitor :(
Provider<SessionTracker> sessionTrackerProvider,
@@ -2196,6 +2199,7 @@
mSensorPrivacyManager = sensorPrivacyManager;
mActiveUnlockConfig = activeUnlockConfiguration;
mLogger = logger;
+ mSimLogger = simLogger;
mUiEventLogger = uiEventLogger;
mSessionTrackerProvider = sessionTrackerProvider;
mTrustManager = trustManager;
@@ -3369,36 +3373,39 @@
}
/**
+ * Removes all valid subscription info from the map for the given slotId.
+ */
+ private void invalidateSlot(int slotId) {
+ Iterator<Map.Entry<Integer, SimData>> iter = mSimDatas.entrySet().iterator();
+ while (iter.hasNext()) {
+ SimData data = iter.next().getValue();
+ if (data.slotId == slotId && SubscriptionManager.isValidSubscriptionId(data.subId)) {
+ mSimLogger.logInvalidSubId(data.subId);
+ iter.remove();
+ }
+ }
+ }
+
+ /**
* Handle {@link #MSG_SIM_STATE_CHANGE}
*/
@VisibleForTesting
void handleSimStateChange(int subId, int slotId, int state) {
Assert.isMainThread();
- mLogger.logSimState(subId, slotId, state);
+ mSimLogger.logSimState(subId, slotId, state);
- boolean becameAbsent = false;
+ boolean becameAbsent = ABSENT_SIM_STATE_LIST.contains(state);
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
- mLogger.w("invalid subId in handleSimStateChange()");
+ mSimLogger.w("invalid subId in handleSimStateChange()");
/* Only handle No SIM(ABSENT) and Card Error(CARD_IO_ERROR) due to
* handleServiceStateChange() handle other case */
- if (state == TelephonyManager.SIM_STATE_ABSENT) {
- updateTelephonyCapable(true);
- // Even though the subscription is not valid anymore, we need to notify that the
- // SIM card was removed so we can update the UI.
- becameAbsent = true;
- for (SimData data : mSimDatas.values()) {
- // Set the SIM state of all SimData associated with that slot to ABSENT se we
- // do not move back into PIN/PUK locked and not detect the change below.
- if (data.slotId == slotId) {
- data.simState = TelephonyManager.SIM_STATE_ABSENT;
- }
- }
- } else if (state == TelephonyManager.SIM_STATE_CARD_IO_ERROR) {
+ if (state == TelephonyManager.SIM_STATE_ABSENT
+ || state == TelephonyManager.SIM_STATE_CARD_IO_ERROR) {
updateTelephonyCapable(true);
}
- }
- becameAbsent |= ABSENT_SIM_STATE_LIST.contains(state);
+ invalidateSlot(slotId);
+ }
// TODO(b/327476182): Preserve SIM_STATE_CARD_IO_ERROR sims in a separate data source.
SimData data = mSimDatas.get(subId);
@@ -3428,10 +3435,10 @@
*/
@VisibleForTesting
void handleServiceStateChange(int subId, ServiceState serviceState) {
- mLogger.logServiceStateChange(subId, serviceState);
+ mSimLogger.logServiceStateChange(subId, serviceState);
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
- mLogger.w("invalid subId in handleServiceStateChange()");
+ mSimLogger.w("invalid subId in handleServiceStateChange()");
return;
} else {
updateTelephonyCapable(true);
@@ -3711,7 +3718,7 @@
*/
@MainThread
public void reportSimUnlocked(int subId) {
- mLogger.logSimUnlocked(subId);
+ mSimLogger.logSimUnlocked(subId);
handleSimStateChange(subId, getSlotId(subId), TelephonyManager.SIM_STATE_READY);
}
@@ -3870,6 +3877,11 @@
private boolean refreshSimState(int subId, int slotId) {
int state = mTelephonyManager.getSimState(slotId);
SimData data = mSimDatas.get(subId);
+
+ if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+ invalidateSlot(slotId);
+ }
+
final boolean changed;
if (data == null) {
data = new SimData(state, slotId, subId);
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 0b58f06..12fc3c2 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -20,8 +20,6 @@
import android.hardware.biometrics.BiometricConstants.LockoutMode
import android.hardware.biometrics.BiometricSourceType
import android.os.PowerManager
-import android.telephony.ServiceState
-import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyManager
@@ -34,7 +32,6 @@
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.core.LogLevel.ERROR
-import com.android.systemui.log.core.LogLevel.INFO
import com.android.systemui.log.core.LogLevel.VERBOSE
import com.android.systemui.log.core.LogLevel.WARNING
import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
@@ -63,7 +60,7 @@
"ActiveUnlock",
DEBUG,
{ str1 = reason },
- { "initiate active unlock triggerReason=$str1" }
+ { "initiate active unlock triggerReason=$str1" },
)
}
@@ -75,7 +72,7 @@
{
"Skip requesting active unlock from wake reason that doesn't trigger face auth" +
" reason=${PowerManager.wakeReasonToString(int1)}"
- }
+ },
)
}
@@ -92,7 +89,7 @@
TAG,
DEBUG,
{ bool1 = deviceProvisioned },
- { "DEVICE_PROVISIONED state = $bool1" }
+ { "DEVICE_PROVISIONED state = $bool1" },
)
}
@@ -108,7 +105,7 @@
str1 = originalErrMsg
int1 = msgId
},
- { "Face error received: $str1 msgId= $int1" }
+ { "Face error received: $str1 msgId= $int1" },
)
}
@@ -117,7 +114,7 @@
TAG,
DEBUG,
{ int1 = authUserId },
- { "Face authenticated for wrong user: $int1" }
+ { "Face authenticated for wrong user: $int1" },
)
}
@@ -130,7 +127,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = authUserId },
- { "Fingerprint authenticated for wrong user: $int1" }
+ { "Fingerprint authenticated for wrong user: $int1" },
)
}
@@ -139,7 +136,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = userId },
- { "Fingerprint disabled by DPM for userId: $int1" }
+ { "Fingerprint disabled by DPM for userId: $int1" },
)
}
@@ -148,7 +145,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = mode },
- { "handleFingerprintLockoutReset: $int1" }
+ { "handleFingerprintLockoutReset: $int1" },
)
}
@@ -157,7 +154,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = fingerprintRunningState },
- { "fingerprintRunningState: $int1" }
+ { "fingerprintRunningState: $int1" },
)
}
@@ -169,7 +166,7 @@
int1 = userId
bool1 = isStrongBiometric
},
- { "Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1" }
+ { "Fingerprint auth successful: userId: $int1, isStrongBiometric: $bool1" },
)
}
@@ -181,7 +178,7 @@
int1 = userId
bool1 = isStrongBiometric
},
- { "Face detected: userId: $int1, isStrongBiometric: $bool1" }
+ { "Face detected: userId: $int1, isStrongBiometric: $bool1" },
)
}
@@ -193,7 +190,7 @@
int1 = userId
bool1 = isStrongBiometric
},
- { "Fingerprint detected: userId: $int1, isStrongBiometric: $bool1" }
+ { "Fingerprint detected: userId: $int1, isStrongBiometric: $bool1" },
)
}
@@ -205,22 +202,13 @@
str1 = originalErrMsg
int1 = msgId
},
- { "Fingerprint error received: $str1 msgId= $int1" }
- )
- }
-
- fun logInvalidSubId(subId: Int) {
- logBuffer.log(
- TAG,
- INFO,
- { int1 = subId },
- { "Previously active sub id $int1 is now invalid, will remove" }
+ { "Fingerprint error received: $str1 msgId= $int1" },
)
}
fun logPrimaryKeyguardBouncerChanged(
primaryBouncerIsOrWillBeShowing: Boolean,
- primaryBouncerFullyShown: Boolean
+ primaryBouncerFullyShown: Boolean,
) {
logBuffer.log(
TAG,
@@ -232,7 +220,7 @@
{
"handlePrimaryBouncerChanged " +
"primaryBouncerIsOrWillBeShowing=$bool1 primaryBouncerFullyShown=$bool2"
- }
+ },
)
}
@@ -249,7 +237,7 @@
bool2 = occluded
bool3 = visible
},
- { "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" }
+ { "keyguardShowingChanged(showing=$bool1 occluded=$bool2 visible=$bool3)" },
)
}
@@ -258,7 +246,7 @@
TAG,
ERROR,
{ int1 = userId },
- { "No Profile Owner or Device Owner supervision app found for User $int1" }
+ { "No Profile Owner or Device Owner supervision app found for User $int1" },
)
}
@@ -279,7 +267,7 @@
int2 = delay
str1 = "$errString"
},
- { "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1" }
+ { "Fingerprint scheduling retry auth after $int2 ms due to($int1) -> $str1" },
)
}
@@ -288,7 +276,7 @@
TAG,
WARNING,
{ int1 = retryCount },
- { "Retrying fingerprint attempt: $int1" }
+ { "Retrying fingerprint attempt: $int1" },
)
}
@@ -306,32 +294,7 @@
{
"sendPrimaryBouncerChanged primaryBouncerIsOrWillBeShowing=$bool1 " +
"primaryBouncerFullyShown=$bool2"
- }
- )
- }
-
- fun logServiceStateChange(subId: Int, serviceState: ServiceState?) {
- logBuffer.log(
- TAG,
- DEBUG,
- {
- int1 = subId
- str1 = "$serviceState"
},
- { "handleServiceStateChange(subId=$int1, serviceState=$str1)" }
- )
- }
-
- fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) {
- logBuffer.log(
- TAG,
- VERBOSE,
- {
- str1 = action
- str2 = "$serviceState"
- int1 = subId
- },
- { "action $str1 serviceState=$str2 subId=$int1" }
)
}
@@ -344,51 +307,16 @@
str1 = intent.getStringExtra(TelephonyManager.EXTRA_SPN)
str2 = intent.getStringExtra(TelephonyManager.EXTRA_PLMN)
},
- { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" }
+ { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" },
)
}
- fun logSimState(subId: Int, slotId: Int, state: Int) {
- logBuffer.log(
- TAG,
- DEBUG,
- {
- int1 = subId
- int2 = slotId
- long1 = state.toLong()
- },
- { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" }
- )
- }
-
- fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) {
- logBuffer.log(
- TAG,
- VERBOSE,
- {
- str1 = action
- str2 = extraSimState
- int1 = slotId
- int2 = subId
- },
- { "action $str1 state: $str2 slotId: $int1 subid: $int2" }
- )
- }
-
- fun logSimUnlocked(subId: Int) {
- logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
- }
-
- fun logSubInfo(subInfo: SubscriptionInfo?) {
- logBuffer.log(TAG, DEBUG, { str1 = "$subInfo" }, { "SubInfo:$str1" })
- }
-
fun logTimeFormatChanged(newTimeFormat: String?) {
logBuffer.log(
TAG,
DEBUG,
{ str1 = newTimeFormat },
- { "handleTimeFormatUpdate timeFormat=$str1" }
+ { "handleTimeFormatUpdate timeFormat=$str1" },
)
}
@@ -402,7 +330,7 @@
fun logUnexpectedFpCancellationSignalState(
fingerprintRunningState: Int,
- unlockPossible: Boolean
+ unlockPossible: Boolean,
) {
logBuffer.log(
TAG,
@@ -414,7 +342,7 @@
{
"Cancellation signal is not null, high chance of bug in " +
"fp auth lifecycle management. FP state: $int1, unlockPossible: $bool1"
- }
+ },
)
}
@@ -425,7 +353,7 @@
fun logUserRequestedUnlock(
requestOrigin: ActiveUnlockConfig.ActiveUnlockRequestOrigin,
reason: String?,
- dismissKeyguard: Boolean
+ dismissKeyguard: Boolean,
) {
logBuffer.log(
"ActiveUnlock",
@@ -435,7 +363,7 @@
str2 = reason
bool1 = dismissKeyguard
},
- { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" }
+ { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" },
)
}
@@ -443,7 +371,7 @@
flags: Int,
newlyUnlocked: Boolean,
userId: Int,
- message: String?
+ message: String?,
) {
logBuffer.log(
TAG,
@@ -457,7 +385,7 @@
{
"trustGrantedWithFlags[user=$int2] newlyUnlocked=$bool1 " +
"flags=${TrustGrantFlags(int1)} message=$str1"
- }
+ },
)
}
@@ -470,7 +398,7 @@
bool2 = isNowTrusted
int1 = userId
},
- { "onTrustChanged[user=$int1] wasTrusted=$bool1 isNowTrusted=$bool2" }
+ { "onTrustChanged[user=$int1] wasTrusted=$bool1 isNowTrusted=$bool2" },
)
}
@@ -478,7 +406,7 @@
secure: Boolean,
canDismissLockScreen: Boolean,
trusted: Boolean,
- trustManaged: Boolean
+ trustManaged: Boolean,
) {
logBuffer.log(
"KeyguardState",
@@ -492,7 +420,7 @@
{
"#update secure=$bool1 canDismissKeyguard=$bool2" +
" trusted=$bool3 trustManaged=$bool4"
- }
+ },
)
}
@@ -501,7 +429,7 @@
TAG,
VERBOSE,
{ bool1 = assistantVisible },
- { "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1" }
+ { "TaskStackChanged for ACTIVITY_TYPE_ASSISTANT, assistant visible: $bool1" },
)
}
@@ -510,7 +438,7 @@
TAG,
VERBOSE,
{ bool1 = allow },
- { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" }
+ { "allowFingerprintOnCurrentOccludingActivityChanged: $bool1" },
)
}
@@ -519,7 +447,7 @@
TAG,
VERBOSE,
{ bool1 = assistantVisible },
- { "Updating mAssistantVisible to new value: $bool1" }
+ { "Updating mAssistantVisible to new value: $bool1" },
)
}
@@ -531,7 +459,7 @@
bool1 = isStrongBiometric
int1 = userId
},
- { "reporting successful biometric unlock: isStrongBiometric: $bool1, userId: $int1" }
+ { "reporting successful biometric unlock: isStrongBiometric: $bool1, userId: $int1" },
)
}
@@ -543,7 +471,7 @@
{
"MSG_BIOMETRIC_AUTHENTICATION_CONTINUE already queued up, " +
"ignoring updating FP listening state to $int1"
- }
+ },
)
}
@@ -551,7 +479,7 @@
userId: Int,
oldValue: Boolean,
newValue: Boolean,
- context: String
+ context: String,
) {
logBuffer.log(
TAG,
@@ -568,7 +496,7 @@
"old: $bool1, " +
"new: $bool2 " +
"context: $str1"
- }
+ },
)
}
@@ -591,7 +519,7 @@
"plugged=$str1, " +
"chargingStatus=$int2, " +
"maxChargingWattage= $long2}"
- }
+ },
)
}
@@ -604,7 +532,7 @@
TAG,
DEBUG,
{ str1 = "$biometricSourceType" },
- { "notifying about enrollments changed: $str1" }
+ { "notifying about enrollments changed: $str1" },
)
}
@@ -616,7 +544,7 @@
int1 = userId
str1 = context
},
- { "userCurrentlySwitching: $str1, userId: $int1" }
+ { "userCurrentlySwitching: $str1, userId: $int1" },
)
}
@@ -628,7 +556,7 @@
int1 = userId
str1 = context
},
- { "userSwitchComplete: $str1, userId: $int1" }
+ { "userSwitchComplete: $str1, userId: $int1" },
)
}
@@ -637,7 +565,7 @@
FP_LOG_TAG,
DEBUG,
{ int1 = acquireInfo },
- { "fingerprint acquire message: $int1" }
+ { "fingerprint acquire message: $int1" },
)
}
@@ -646,7 +574,7 @@
TAG,
DEBUG,
{ bool1 = keepUnlocked },
- { "keepUnlockedOnFold changed to: $bool1" }
+ { "keepUnlockedOnFold changed to: $bool1" },
)
}
@@ -662,7 +590,7 @@
int1 = userId
bool1 = isUnlocked
},
- { "userStopped userId: $int1 isUnlocked: $bool1" }
+ { "userStopped userId: $int1 isUnlocked: $bool1" },
)
}
@@ -678,7 +606,7 @@
int1 = userId
bool1 = isUnlocked
},
- { "userUnlockedInitialState userId: $int1 isUnlocked: $bool1" }
+ { "userUnlockedInitialState userId: $int1 isUnlocked: $bool1" },
)
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/SimLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/SimLogger.kt
new file mode 100644
index 0000000..a81698b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/SimLogger.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.keyguard.logging
+
+import android.content.Intent
+import android.telephony.ServiceState
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyManager
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.core.LogLevel.VERBOSE
+import com.android.systemui.log.core.LogLevel.WARNING
+import com.android.systemui.log.dagger.SimLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "SimLog"
+
+/** Helper class for logging for SIM events */
+class SimLogger @Inject constructor(@SimLog private val logBuffer: LogBuffer) {
+ fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+ fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+ fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE)
+
+ fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+ fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg)
+
+ fun logInvalidSubId(subId: Int) {
+ logBuffer.log(
+ TAG,
+ INFO,
+ { int1 = subId },
+ { "Previously active sub id $int1 is now invalid, will remove" },
+ )
+ }
+
+ fun logServiceStateChange(subId: Int, serviceState: ServiceState?) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = subId
+ str1 = "$serviceState"
+ },
+ { "handleServiceStateChange(subId=$int1, serviceState=$str1)" },
+ )
+ }
+
+ fun logServiceStateIntent(action: String?, serviceState: ServiceState?, subId: Int) {
+ logBuffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = action
+ str2 = "$serviceState"
+ int1 = subId
+ },
+ { "action $str1 serviceState=$str2 subId=$int1" },
+ )
+ }
+
+ fun logServiceProvidersUpdated(intent: Intent) {
+ logBuffer.log(
+ TAG,
+ VERBOSE,
+ {
+ int1 = intent.getIntExtra(EXTRA_SUBSCRIPTION_INDEX, INVALID_SUBSCRIPTION_ID)
+ str1 = intent.getStringExtra(TelephonyManager.EXTRA_SPN)
+ str2 = intent.getStringExtra(TelephonyManager.EXTRA_PLMN)
+ },
+ { "action SERVICE_PROVIDERS_UPDATED subId=$int1 spn=$str1 plmn=$str2" },
+ )
+ }
+
+ fun logSimState(subId: Int, slotId: Int, state: Int) {
+ logBuffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = subId
+ int2 = slotId
+ long1 = state.toLong()
+ },
+ { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" },
+ )
+ }
+
+ fun logSimStateFromIntent(action: String?, extraSimState: String?, slotId: Int, subId: Int) {
+ logBuffer.log(
+ TAG,
+ VERBOSE,
+ {
+ str1 = action
+ str2 = extraSimState
+ int1 = slotId
+ int2 = subId
+ },
+ { "action $str1 state: $str2 slotId: $int1 subid: $int2" },
+ )
+ }
+
+ fun logSimUnlocked(subId: Int) {
+ logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
+ }
+
+ fun logSubInfo(subInfo: SubscriptionInfo?) {
+ logBuffer.log(TAG, DEBUG, { str1 = "$subInfo" }, { "SubInfo:$str1" })
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index b39aae9..a5bd559 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -19,6 +19,7 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
+import static android.view.Display.INVALID_DISPLAY;
import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
@@ -54,6 +55,7 @@
import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback;
import android.os.Handler;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.util.RotationUtils;
@@ -211,9 +213,13 @@
}
};
- private void closeDialog(String reason) {
+ private void closeDialog(String reasonString) {
+ closeDialog(BiometricPrompt.DISMISSED_REASON_USER_CANCEL, reasonString);
+ }
+
+ private void closeDialog(@DismissedReason int reason, String reasonString) {
if (isShowing()) {
- Log.i(TAG, "Close BP, reason :" + reason);
+ Log.i(TAG, "Close BP, reason :" + reasonString);
mCurrentDialog.dismissWithoutCallback(true /* animate */);
mCurrentDialog = null;
@@ -223,8 +229,7 @@
try {
if (mReceiver != null) {
- mReceiver.onDialogDismissed(BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
- null /* credentialAttestation */);
+ mReceiver.onDialogDismissed(reason, null /* credentialAttestation */);
mReceiver = null;
}
} catch (RemoteException e) {
@@ -251,25 +256,7 @@
private void cancelIfOwnerIsNotInForeground() {
mExecution.assertIsMainThread();
- if (mCurrentDialog != null) {
- try {
- mCurrentDialog.dismissWithoutCallback(true /* animate */);
- mCurrentDialog = null;
-
- for (Callback cb : mCallbacks) {
- cb.onBiometricPromptDismissed();
- }
-
- if (mReceiver != null) {
- mReceiver.onDialogDismissed(
- BiometricPrompt.DISMISSED_REASON_USER_CANCEL,
- null /* credentialAttestation */);
- mReceiver = null;
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Remote exception", e);
- }
- }
+ closeDialog("owner not in foreground");
}
/**
@@ -1271,10 +1258,44 @@
if (!promptInfo.isAllowBackgroundAuthentication() && isOwnerInBackground()) {
cancelIfOwnerIsNotInForeground();
} else {
- mCurrentDialog.show(mWindowManager);
+ WindowManager wm = getWindowManagerForUser(userId);
+ if (wm != null) {
+ mCurrentDialog.show(wm);
+ } else {
+ closeDialog(BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM,
+ "unable to get WM instance for user");
+ }
}
}
+ @Nullable
+ private WindowManager getWindowManagerForUser(int userId) {
+ if (!mUserManager.isVisibleBackgroundUsersSupported()) {
+ return mWindowManager;
+ }
+ UserManager um = mContext.createContextAsUser(UserHandle.of(userId),
+ 0 /* flags */).getSystemService(UserManager.class);
+ if (um == null) {
+ Log.e(TAG, "unable to get UserManager for user=" + userId);
+ return null;
+ }
+ if (!um.isUserVisible()) {
+ // not visible user - use default window manager
+ return mWindowManager;
+ }
+ int displayId = um.getMainDisplayIdAssignedToUser();
+ if (displayId == INVALID_DISPLAY) {
+ Log.e(TAG, "unable to get display assigned to user=" + userId);
+ return null;
+ }
+ Display display = mDisplayManager.getDisplay(displayId);
+ if (display == null) {
+ Log.e(TAG, "unable to get Display for user=" + userId);
+ return null;
+ }
+ return mContext.createDisplayContext(display).getSystemService(WindowManager.class);
+ }
+
private void onDialogDismissed(@DismissedReason int reason) {
if (DEBUG) Log.d(TAG, "onDialogDismissed: " + reason);
if (mCurrentDialog == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index 73f75a4..18446f02 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -18,13 +18,11 @@
import android.animation.Animator
import android.animation.AnimatorSet
-import android.animation.ValueAnimator
import android.graphics.Outline
import android.graphics.Rect
import android.transition.AutoTransition
import android.transition.TransitionManager
import android.util.TypedValue
-import android.view.Surface
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
@@ -52,7 +50,6 @@
import com.android.systemui.res.R
import kotlin.math.abs
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/** Helper for [BiometricViewBinder] to handle resize transitions. */
@@ -98,7 +95,7 @@
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1f,
- view.resources.displayMetrics
+ view.resources.displayMetrics,
)
val cornerRadiusPx = (pxToDp * cornerRadius).toInt()
@@ -114,7 +111,7 @@
0,
view.width + cornerRadiusPx,
view.height,
- cornerRadiusPx.toFloat()
+ cornerRadiusPx.toFloat(),
)
}
PromptPosition.Left -> {
@@ -123,7 +120,7 @@
0,
view.width,
view.height,
- cornerRadiusPx.toFloat()
+ cornerRadiusPx.toFloat(),
)
}
PromptPosition.Bottom,
@@ -133,7 +130,7 @@
0,
view.width,
view.height + cornerRadiusPx,
- cornerRadiusPx.toFloat()
+ cornerRadiusPx.toFloat(),
)
}
}
@@ -160,16 +157,13 @@
fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
if (hideSensorIcon) {
smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
}
}
@@ -189,24 +183,24 @@
R.id.biometric_icon,
ConstraintSet.LEFT,
ConstraintSet.PARENT_ID,
- ConstraintSet.LEFT
+ ConstraintSet.LEFT,
)
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.LEFT,
- position.left
+ position.left,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT)
smallConstraintSet.connect(
R.id.biometric_icon,
ConstraintSet.LEFT,
ConstraintSet.PARENT_ID,
- ConstraintSet.LEFT
+ ConstraintSet.LEFT,
)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.LEFT,
- position.left
+ position.left,
)
}
if (position.top != 0) {
@@ -216,13 +210,13 @@
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.TOP,
- position.top
+ position.top,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.TOP,
- position.top
+ position.top,
)
}
if (position.right != 0) {
@@ -233,24 +227,24 @@
R.id.biometric_icon,
ConstraintSet.RIGHT,
ConstraintSet.PARENT_ID,
- ConstraintSet.RIGHT
+ ConstraintSet.RIGHT,
)
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.RIGHT,
- position.right
+ position.right,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT)
smallConstraintSet.connect(
R.id.biometric_icon,
ConstraintSet.RIGHT,
ConstraintSet.PARENT_ID,
- ConstraintSet.RIGHT
+ ConstraintSet.RIGHT,
)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.RIGHT,
- position.right
+ position.right,
)
}
if (position.bottom != 0) {
@@ -260,13 +254,13 @@
mediumConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.BOTTOM,
- position.bottom
+ position.bottom,
)
smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
smallConstraintSet.setMargin(
R.id.biometric_icon,
ConstraintSet.BOTTOM,
- position.bottom
+ position.bottom,
)
}
iconHolderView.layoutParams = iconParams
@@ -305,11 +299,11 @@
} else if (bounds.right < 0) {
mediumConstraintSet.setGuidelineBegin(
rightGuideline.id,
- abs(bounds.right)
+ abs(bounds.right),
)
smallConstraintSet.setGuidelineBegin(
rightGuideline.id,
- abs(bounds.right)
+ abs(bounds.right),
)
}
@@ -362,13 +356,13 @@
R.id.scrollView,
ConstraintSet.LEFT,
R.id.midGuideline,
- ConstraintSet.LEFT
+ ConstraintSet.LEFT,
)
flipConstraintSet.connect(
R.id.scrollView,
ConstraintSet.RIGHT,
R.id.rightGuideline,
- ConstraintSet.RIGHT
+ ConstraintSet.RIGHT,
)
} else if (position.isTop) {
// Top position is only used for 180 rotation Udfps
@@ -377,24 +371,24 @@
R.id.scrollView,
ConstraintSet.TOP,
R.id.indicator,
- ConstraintSet.BOTTOM
+ ConstraintSet.BOTTOM,
)
mediumConstraintSet.connect(
R.id.scrollView,
ConstraintSet.BOTTOM,
R.id.button_bar,
- ConstraintSet.TOP
+ ConstraintSet.TOP,
)
mediumConstraintSet.connect(
R.id.panel,
ConstraintSet.TOP,
R.id.biometric_icon,
- ConstraintSet.TOP
+ ConstraintSet.TOP,
)
mediumConstraintSet.setMargin(
R.id.panel,
ConstraintSet.TOP,
- (-24 * pxToDp).toInt()
+ (-24 * pxToDp).toInt(),
)
mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/util/BouncerTestUtils.kt b/packages/SystemUI/src/com/android/systemui/bouncer/util/BouncerTestUtils.kt
new file mode 100644
index 0000000..08a79c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/util/BouncerTestUtils.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.util
+
+import android.app.ActivityManager
+import android.content.res.Resources
+import com.android.systemui.res.R
+import java.io.File
+
+private const val ENABLE_MENU_KEY_FILE = "/data/local/enable_menu_key"
+
+/**
+ * In general, we enable unlocking the insecure keyguard with the menu key. However, there are some
+ * cases where we wish to disable it, notably when the menu button placement or technology is prone
+ * to false positives.
+ *
+ * @return true if the menu key should be enabled
+ */
+fun Resources.shouldEnableMenuKey(): Boolean {
+ val configDisabled = getBoolean(R.bool.config_disableMenuKeyInLockScreen)
+ val isTestHarness = ActivityManager.isRunningInTestHarness()
+ val fileOverride = File(ENABLE_MENU_KEY_FILE).exists()
+ return !configDisabled || isTestHarness || fileOverride
+}
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index aabfbd1..65c01ed 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -710,9 +710,16 @@
@Override
public void onShareButtonTapped() {
if (clipboardSharedTransitions()) {
- if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) {
- finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
- IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+ switch (mClipboardModel.getType()) {
+ case TEXT:
+ case URI:
+ finish(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+ IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+ break;
+ case IMAGE:
+ finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
+ IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
+ break;
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
index 8f1854f..17f4f0c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt
@@ -26,7 +26,7 @@
import androidx.sqlite.db.SupportSQLiteDatabase
import com.android.systemui.res.R
-@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 3)
+@Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 4)
abstract class CommunalDatabase : RoomDatabase() {
abstract fun communalWidgetDao(): CommunalWidgetDao
@@ -43,19 +43,16 @@
* @param callback An optional callback registered to the database. Only effective when a
* new instance is created.
*/
- fun getInstance(
- context: Context,
- callback: Callback? = null,
- ): CommunalDatabase {
+ fun getInstance(context: Context, callback: Callback? = null): CommunalDatabase {
if (instance == null) {
instance =
Room.databaseBuilder(
context,
CommunalDatabase::class.java,
- context.resources.getString(R.string.config_communalDatabase)
+ context.resources.getString(R.string.config_communalDatabase),
)
.also { builder ->
- builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
+ builder.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
builder.fallbackToDestructiveMigration(dropAllTables = true)
callback?.let { callback -> builder.addCallback(callback) }
}
@@ -103,5 +100,21 @@
)
}
}
+
+ /**
+ * This migration adds a span_y column to the communal_widget_table and sets its default
+ * value to 3.
+ */
+ @VisibleForTesting
+ val MIGRATION_3_4 =
+ object : Migration(3, 4) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ Log.i(TAG, "Migrating from version 3 to 4")
+ db.execSQL(
+ "ALTER TABLE communal_widget_table " +
+ "ADD COLUMN span_y INTEGER NOT NULL DEFAULT 3"
+ )
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
index e33aead..f9d2a84 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalEntities.kt
@@ -40,6 +40,12 @@
*/
@ColumnInfo(name = "user_serial_number", defaultValue = "$USER_SERIAL_NUMBER_UNDEFINED")
val userSerialNumber: Int,
+
+ /**
+ * The vertical span of the widget. Span_Y default value corresponds to
+ * CommunalContentSize.HALF.span
+ */
+ @ColumnInfo(name = "span_y", defaultValue = "3") val spanY: Int,
) {
companion object {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index 93b86bd..5dd4c1c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -25,6 +25,7 @@
import androidx.room.Transaction
import androidx.sqlite.db.SupportSQLiteDatabase
import com.android.systemui.communal.nano.CommunalHubState
+import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.widgets.CommunalWidgetHost
import com.android.systemui.communal.widgets.CommunalWidgetModule.Companion.DEFAULT_WIDGETS
import com.android.systemui.dagger.SysUISingleton
@@ -153,14 +154,15 @@
@Query(
"INSERT INTO communal_widget_table" +
- "(widget_id, component_name, item_id, user_serial_number) " +
- "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber)"
+ "(widget_id, component_name, item_id, user_serial_number, span_y) " +
+ "VALUES(:widgetId, :componentName, :itemId, :userSerialNumber, :spanY)"
)
fun insertWidget(
widgetId: Int,
componentName: String,
itemId: Long,
userSerialNumber: Int,
+ spanY: Int = 3,
): Long
@Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
@@ -169,6 +171,9 @@
@Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid")
fun updateItemRank(itemUid: Long, order: Int)
+ @Query("UPDATE communal_widget_table SET span_y = :spanY WHERE widget_id = :widgetId")
+ fun updateWidgetSpanY(widgetId: Int, spanY: Int)
+
@Query("DELETE FROM communal_widget_table") fun clearCommunalWidgetsTable()
@Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable()
@@ -189,12 +194,14 @@
provider: ComponentName,
rank: Int? = null,
userSerialNumber: Int,
+ spanY: Int = CommunalContentSize.HALF.span,
): Long {
return addWidget(
widgetId = widgetId,
componentName = provider.flattenToString(),
rank = rank,
userSerialNumber = userSerialNumber,
+ spanY = spanY,
)
}
@@ -204,6 +211,7 @@
componentName: String,
rank: Int? = null,
userSerialNumber: Int,
+ spanY: Int = 3,
): Long {
val widgets = getWidgetsNow()
@@ -224,6 +232,7 @@
componentName = componentName,
itemId = insertItemRank(newRank),
userSerialNumber = userSerialNumber,
+ spanY = spanY,
)
}
@@ -246,7 +255,8 @@
clearCommunalItemRankTable()
state.widgets.forEach {
- addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber)
+ val spanY = if (it.spanY != 0) it.spanY else CommunalContentSize.HALF.span
+ addWidget(it.widgetId, it.componentName, it.rank, it.userSerialNumber, spanY)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 6cdd9ff..3312f3c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -92,6 +92,14 @@
/** Aborts the restore process and removes files from disk if necessary. */
fun abortRestoreWidgets()
+
+ /**
+ * Update the spanY of a widget in the database.
+ *
+ * @param widgetId id of the widget to update.
+ * @param spanY new spanY value for the widget.
+ */
+ fun updateWidgetSpanY(widgetId: Int, spanY: Int)
}
@SysUISingleton
@@ -118,20 +126,30 @@
/** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */
private val widgetEntries: Flow<List<CommunalWidgetEntry>> =
- combine(
- communalWidgetDao.getWidgets(),
- communalWidgetHost.appWidgetProviders,
- ) { entries, providers ->
+ combine(communalWidgetDao.getWidgets(), communalWidgetHost.appWidgetProviders) {
+ entries,
+ providers ->
entries.mapNotNull { (rank, widget) ->
CommunalWidgetEntry(
appWidgetId = widget.widgetId,
componentName = widget.componentName,
rank = rank.rank,
- providerInfo = providers[widget.widgetId]
+ providerInfo = providers[widget.widgetId],
)
}
}
+ override fun updateWidgetSpanY(widgetId: Int, spanY: Int) {
+ bgScope.launch {
+ communalWidgetDao.updateWidgetSpanY(widgetId, spanY)
+ logger.i({ "Updated spanY of widget $int1 to $int2." }) {
+ int1 = widgetId
+ int2 = spanY
+ }
+ backupManager.dataChanged()
+ }
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
widgetEntries
@@ -197,6 +215,7 @@
provider = provider,
rank = rank,
userSerialNumber = userManager.getUserSerialNumber(user.identifier),
+ spanY = 3,
)
backupManager.dataChanged()
} else {
@@ -325,6 +344,7 @@
componentName = restoredWidget.componentName
rank = restoredWidget.rank
userSerialNumber = userManager.getUserSerialNumber(newUser.identifier)
+ spanY = restoredWidget.spanY
}
}
val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() }
@@ -383,6 +403,7 @@
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
rank = entry.rank,
+ spanY = entry.spanY,
)
}
@@ -400,6 +421,7 @@
appWidgetId = entry.appWidgetId,
providerInfo = entry.providerInfo!!,
rank = entry.rank,
+ spanY = entry.spanY,
)
}
@@ -412,6 +434,7 @@
componentName = componentName,
icon = session.icon,
user = session.user,
+ spanY = entry.spanY,
)
} else {
null
@@ -423,5 +446,6 @@
val componentName: String,
val rank: Int,
var providerInfo: AppWidgetProviderInfo? = null,
+ var spanY: Int = 3,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
index bc14ae1..7602a7a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
+++ b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto
@@ -38,5 +38,8 @@
// Serial number of the user associated with the widget.
int32 user_serial_number = 4;
+
+ // The vertical span of the widget
+ int32 span_y = 5;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index 63b1a14..bcbc8f6 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -31,6 +31,7 @@
override val appWidgetId: Int,
val providerInfo: AppWidgetProviderInfo,
override val rank: Int,
+ val spanY: Int = 3,
) : CommunalWidgetContentModel
/** Widget is pending installation */
@@ -40,5 +41,6 @@
val componentName: ComponentName,
val icon: Bitmap?,
val user: UserHandle,
+ val spanY: Int = 3,
) : CommunalWidgetContentModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt
index 099e3fc..4b9ac1d 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt
@@ -21,9 +21,10 @@
import android.view.View
import android.widget.TextClock
import com.android.internal.util.Preconditions
-import com.android.systemui.res.R
+import com.android.systemui.Flags
import com.android.systemui.complication.DreamClockTimeComplication
import com.android.systemui.complication.DreamClockTimeComplication.DreamClockTimeViewHolder
+import com.android.systemui.res.R
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@@ -71,9 +72,13 @@
/* root = */ null,
/* attachToRoot = */ false,
) as TextClock,
- "R.layout.dream_overlay_complication_clock_time did not properly inflate"
+ "R.layout.dream_overlay_complication_clock_time did not properly inflate",
)
- view.setFontVariationSettings(TAG_WEIGHT + WEIGHT)
+ if (Flags.dreamOverlayUpdatedFont()) {
+ view.setFontVariationSettings("'wght' 600, 'opsz' 96")
+ } else {
+ view.setFontVariationSettings(TAG_WEIGHT + WEIGHT)
+ }
return view
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index a94fbd9..a5b2277 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -56,6 +56,7 @@
import com.android.systemui.scene.SceneContainerFrameworkModule;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
import com.android.systemui.settings.MultiUserUtilsModule;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.ShadeModule;
import com.android.systemui.startable.Dependencies;
@@ -178,9 +179,9 @@
@Provides
@SysUISingleton
static IndividualSensorPrivacyController provideIndividualSensorPrivacyController(
- SensorPrivacyManager sensorPrivacyManager) {
+ SensorPrivacyManager sensorPrivacyManager, UserTracker userTracker) {
IndividualSensorPrivacyController spC = new IndividualSensorPrivacyControllerImpl(
- sensorPrivacyManager);
+ sensorPrivacyManager, userTracker);
spC.init();
return spC;
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
new file mode 100644
index 0000000..dc07cca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FocusedDisplayRepository.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.display.data.repository
+
+import android.annotation.MainThread
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.FocusedDisplayRepoLog
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import com.android.wm.shell.shared.FocusTransitionListener
+import com.android.wm.shell.shared.ShellTransitions
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository tracking display focus. */
+@SysUISingleton
+@MainThread
+class FocusedDisplayRepository
+@Inject
+constructor(
+ @Application val scope: CoroutineScope,
+ @Main private val mainExecutor: Executor,
+ transitions: ShellTransitions,
+ @FocusedDisplayRepoLog logBuffer: LogBuffer,
+) {
+ val focusedTask: Flow<Int> =
+ conflatedCallbackFlow {
+ val listener = FocusTransitionListener { displayId -> trySend(displayId) }
+ transitions.setFocusTransitionListener(listener, mainExecutor)
+ awaitClose { transitions.unsetFocusTransitionListener(listener) }
+ }
+ .onEach {
+ logBuffer.log(
+ "FocusedDisplayRepository",
+ LogLevel.INFO,
+ { str1 = it.toString() },
+ { "Newly focused display: $str1" },
+ )
+ }
+
+ /** Provides the currently focused display. */
+ val focusedDisplayId: StateFlow<Int>
+ get() = focusedTask.stateIn(scope, SharingStarted.Eagerly, DEFAULT_DISPLAY)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
index 5a008bd..7711c48 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/data/repository/InputDeviceRepository.kt
@@ -38,7 +38,7 @@
constructor(
@Background private val backgroundHandler: Handler,
@Background private val backgroundScope: CoroutineScope,
- private val inputManager: InputManager
+ private val inputManager: InputManager,
) {
sealed interface DeviceChange
@@ -50,11 +50,11 @@
data object FreshStart : DeviceChange
/**
- * Emits collection of all currently connected keyboards and what was the last [DeviceChange].
- * It emits collection so that every new subscriber to this SharedFlow can get latest state of
- * all keyboards. Otherwise we might get into situation where subscriber timing on
- * initialization matter and later subscriber will only get latest device and will miss all
- * previous devices.
+ * Emits collection of all currently connected input devices and what was the last
+ * [DeviceChange]. It emits collection so that every new subscriber to this SharedFlow can get
+ * latest state of all input devices. Otherwise we might get into situation where subscriber
+ * timing on initialization matter and later subscriber will only get latest device and will
+ * miss all previous devices.
*/
// TODO(b/351984587): Replace with StateFlow
@SuppressLint("SharedFlowCreation")
@@ -79,11 +79,7 @@
inputManager.registerInputDeviceListener(listener, backgroundHandler)
awaitClose { inputManager.unregisterInputDeviceListener(listener) }
}
- .shareIn(
- scope = backgroundScope,
- started = SharingStarted.Lazily,
- replay = 1,
- )
+ .shareIn(scope = backgroundScope, started = SharingStarted.Lazily, replay = 1)
private fun <T> SendChannel<T>.sendWithLogging(element: T) {
trySendWithFailureLogging(element, TAG)
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
index f49cfdd..021c069 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt
@@ -50,6 +50,8 @@
private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+ override val connectedKeyboards: Flow<Set<Keyboard>> = MutableStateFlow(emptySet())
+
init {
Log.i(TAG, "initializing shell command $COMMAND")
commandRegistry.registerCommand(COMMAND) { KeyboardCommand() }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index a20dfa5..3329fe2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -61,6 +61,9 @@
*/
val newlyConnectedKeyboard: Flow<Keyboard>
+ /** Emits set of currently connected keyboards */
+ val connectedKeyboards: Flow<Set<Keyboard>>
+
/**
* Emits [BacklightModel] whenever user changes backlight level from keyboard press. Can only
* happen when physical keyboard is connected
@@ -74,7 +77,7 @@
constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val inputManager: InputManager,
- inputDeviceRepository: InputDeviceRepository
+ inputDeviceRepository: InputDeviceRepository,
) : KeyboardRepository {
@FlowPreview
@@ -93,6 +96,13 @@
.mapNotNull { deviceIdToKeyboard(it) }
.flowOn(backgroundDispatcher)
+ override val connectedKeyboards: Flow<Set<Keyboard>> =
+ inputDeviceRepository.deviceChange
+ .map { (deviceIds, _) -> deviceIds }
+ .map { deviceIds -> deviceIds.filter { isPhysicalFullKeyboard(it) } }
+ .distinctUntilChanged()
+ .map { deviceIds -> deviceIds.mapNotNull { deviceIdToKeyboard(it) }.toSet() }
+
override val isAnyKeyboardConnected: Flow<Boolean> =
inputDeviceRepository.deviceChange
.map { (ids, _) -> ids.any { id -> isPhysicalFullKeyboard(id) } }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 9443570..1497026 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -19,10 +19,12 @@
import com.android.keyguard.logging.ScrimLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.DEFAULT_REVEAL_DURATION
import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.shared.model.ScreenPowerState
+import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.statusbar.LightRevealEffect
import com.android.systemui.util.kotlin.sample
import dagger.Lazy
@@ -50,11 +52,29 @@
scope.launch {
transitionInteractor.startedKeyguardTransitionStep.collect {
scrimLogger.d(TAG, "listenForStartedKeyguardTransitionStep", it)
- lightRevealScrimRepository.startRevealAmountAnimator(willBeRevealedInState(it.to))
+ val animationDuration =
+ if (it.to == KeyguardState.AOD && isLastSleepDueToFold) {
+ // Do not animate the scrim when folding as we want to cover the screen
+ // with the scrim immediately while displays are switching.
+ // This is needed to play the fold to AOD animation which starts with
+ // fully black screen (see FoldAodAnimationController)
+ 0L
+ } else {
+ DEFAULT_REVEAL_DURATION
+ }
+
+ lightRevealScrimRepository.startRevealAmountAnimator(
+ willBeRevealedInState(it.to),
+ duration = animationDuration
+ )
}
}
}
+ private val isLastSleepDueToFold: Boolean
+ get() = powerInteractor.get().detailedWakefulness.value
+ .lastSleepReason == WakeSleepReason.FOLD
+
/**
* Whenever a keyguard transition starts, sample the latest reveal effect from the repository
* and use that for the starting transition.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index 8386628..57cb10f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -121,7 +121,10 @@
private fun applyClockDefaultConstraints(context: Context, constraints: ConstraintSet) {
constraints.apply {
constrainWidth(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
- constrainHeight(R.id.lockscreen_clock_view_large, ConstraintSet.MATCH_CONSTRAINT)
+ // The following two lines make lockscreen_clock_view_large is constrained to available
+ // height when it goes beyond constraints; otherwise, it use WRAP_CONTENT
+ constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
+ constrainMaxHeight(R.id.lockscreen_clock_view_large, 0)
val largeClockTopMargin =
SystemBarUtils.getStatusBarHeight(context) +
context.resources.getDimensionPixelSize(
@@ -138,7 +141,7 @@
R.id.lockscreen_clock_view_large,
ConstraintSet.END,
PARENT_ID,
- ConstraintSet.END
+ ConstraintSet.END,
)
// In preview, we'll show UDFPS icon for UDFPS devices
@@ -160,14 +163,14 @@
BOTTOM,
PARENT_ID,
BOTTOM,
- clockBottomMargin
+ clockBottomMargin,
)
}
constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
constrainHeight(
R.id.lockscreen_clock_view,
- context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
+ context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height),
)
connect(
R.id.lockscreen_clock_view,
@@ -175,7 +178,7 @@
PARENT_ID,
START,
context.resources.getDimensionPixelSize(customizationR.dimen.clock_padding_start) +
- context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal),
)
val smallClockTopMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
@@ -188,7 +191,7 @@
context: Context,
rootView: ConstraintLayout,
previewClock: ClockController,
- viewModel: KeyguardPreviewClockViewModel
+ viewModel: KeyguardPreviewClockViewModel,
) {
val cs = ConstraintSet().apply { clone(rootView) }
applyClockDefaultConstraints(context, cs)
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/FocusedDisplayRepoLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/FocusedDisplayRepoLog.kt
new file mode 100644
index 0000000..302f962
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/FocusedDisplayRepoLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for display metrics related logging. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FocusedDisplayRepoLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 2053b53..4e975ff 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -480,6 +480,16 @@
}
/**
+ * Provides a {@link LogBuffer} for use by SIM events.
+ */
+ @Provides
+ @SysUISingleton
+ @SimLog
+ public static LogBuffer provideSimLogBuffer(LogBufferFactory factory) {
+ return factory.create("SimLog", 500);
+ }
+
+ /**
* Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
*/
@Provides
@@ -655,6 +665,14 @@
return factory.create("DisplayMetricsRepo", 50);
}
+ /** Provides a {@link LogBuffer} for focus related logs. */
+ @Provides
+ @SysUISingleton
+ @FocusedDisplayRepoLog
+ public static LogBuffer provideFocusedDisplayRepoLogBuffer(LogBufferFactory factory) {
+ return factory.create("FocusedDisplayRepo", 50);
+ }
+
/** Provides a {@link LogBuffer} for the scene framework. */
@Provides
@SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SimLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/SimLog.kt
new file mode 100644
index 0000000..64fc6e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SimLog.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for SIM events. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class SimLog
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 222d783..4528b04 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -417,6 +417,7 @@
override fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
if (useQsMediaPlayer && isMediaNotification(sbn)) {
var isNewlyActiveEntry = false
+ var isConvertingToActive = false
Assert.isMainThread()
val oldKey = findExistingEntry(key, sbn.packageName)
if (oldKey == null) {
@@ -433,9 +434,10 @@
// Resume -> active conversion; move to new key
val oldData = mediaEntries.remove(oldKey)!!
isNewlyActiveEntry = true
+ isConvertingToActive = true
mediaEntries.put(key, oldData)
}
- loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaData(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
} else {
onNotificationRemoved(key)
}
@@ -535,10 +537,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) {
if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
applicationScope.launch {
- loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
}
} else {
backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
@@ -550,10 +553,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) =
withContext(backgroundDispatcher) {
val lastActive = systemClock.elapsedRealtime()
- val result = mediaDataLoader.get().loadMediaData(key, sbn)
+ val result = mediaDataLoader.get().loadMediaData(key, sbn, isConvertingToActive)
if (result == null) {
Log.d(TAG, "No result from loadMediaData")
return@withContext
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index 7b55dac8..7b8703d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -111,16 +111,26 @@
* If a new [loadMediaData] is issued while existing load is in progress, the existing (old)
* load will be cancelled.
*/
- suspend fun loadMediaData(key: String, sbn: StatusBarNotification): MediaDataLoaderResult? {
- val loadMediaJob = backgroundScope.async { loadMediaDataInBackground(key, sbn) }
+ suspend fun loadMediaData(
+ key: String,
+ sbn: StatusBarNotification,
+ isConvertingToActive: Boolean = false,
+ ): MediaDataLoaderResult? {
+ val loadMediaJob =
+ backgroundScope.async { loadMediaDataInBackground(key, sbn, isConvertingToActive) }
loadMediaJob.invokeOnCompletion {
// We need to make sure we're removing THIS job after cancellation, not
// a job that we created later.
mediaProcessingJobs.remove(key, loadMediaJob)
}
- val existingJob = mediaProcessingJobs.put(key, loadMediaJob)
+ var existingJob: Job? = null
+ // Do not cancel loading jobs that convert resume players to active.
+ if (!isConvertingToActive) {
+ existingJob = mediaProcessingJobs.put(key, loadMediaJob)
+ existingJob?.cancel("New processing job incoming.")
+ }
logD(TAG) { "Loading media data for $key... / existing job: $existingJob" }
- existingJob?.cancel("New processing job incoming.")
+
return loadMediaJob.await()
}
@@ -129,12 +139,16 @@
private suspend fun loadMediaDataInBackground(
key: String,
sbn: StatusBarNotification,
+ isConvertingToActive: Boolean = false,
): MediaDataLoaderResult? =
traceCoroutine("MediaDataLoader#loadMediaData") {
// We have apps spamming us with quick notification updates which can cause
// us to spend significant CPU time loading duplicate data. This debounces
// those requests at the cost of a bit of latency.
- delay(DEBOUNCE_DELAY_MS)
+ // No delay needed to load jobs converting resume players to active.
+ if (!isConvertingToActive) {
+ delay(DEBOUNCE_DELAY_MS)
+ }
val token =
sbn.notification.extras.getParcelable(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index fd7b6dc..affc7b7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -330,6 +330,7 @@
fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
if (useQsMediaPlayer && isMediaNotification(sbn)) {
var isNewlyActiveEntry = false
+ var isConvertingToActive = false
Assert.isMainThread()
val oldKey = findExistingEntry(key, sbn.packageName)
if (oldKey == null) {
@@ -347,9 +348,10 @@
// Resume -> active conversion; move to new key
val oldData = mediaDataRepository.removeMediaEntry(oldKey)!!
isNewlyActiveEntry = true
+ isConvertingToActive = true
mediaDataRepository.addMediaEntry(key, oldData)
}
- loadMediaData(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaData(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
} else {
onNotificationRemoved(key)
}
@@ -488,10 +490,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) {
if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
applicationScope.launch {
- loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry)
+ loadMediaDataWithLoader(key, sbn, oldKey, isNewlyActiveEntry, isConvertingToActive)
}
} else {
backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) }
@@ -835,10 +838,11 @@
sbn: StatusBarNotification,
oldKey: String?,
isNewlyActiveEntry: Boolean = false,
+ isConvertingToActive: Boolean = false,
) =
withContext(backgroundDispatcher) {
val lastActive = systemClock.elapsedRealtime()
- val result = mediaDataLoader.get().loadMediaData(key, sbn)
+ val result = mediaDataLoader.get().loadMediaData(key, sbn, isConvertingToActive)
if (result == null) {
Log.d(TAG, "No result from loadMediaData")
return@withContext
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
index 078d534..f563f87 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverLogger.kt
@@ -26,18 +26,11 @@
/** A logger for all events related to the media tap-to-transfer receiver experience. */
@SysUISingleton
-class MediaTttReceiverLogger
-@Inject
-constructor(
- @MediaTttReceiverLogBuffer buffer: LogBuffer,
-) : TemporaryViewLogger<ChipReceiverInfo>(buffer, TAG) {
+class MediaTttReceiverLogger @Inject constructor(@MediaTttReceiverLogBuffer buffer: LogBuffer) :
+ TemporaryViewLogger<ChipReceiverInfo>(buffer, TAG) {
/** Logs a change in the chip state for the given [mediaRouteId]. */
- fun logStateChange(
- stateName: String,
- mediaRouteId: String,
- packageName: String?,
- ) {
+ fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) {
MediaTttLoggerUtils.logStateChange(buffer, TAG, stateName, mediaRouteId, packageName)
}
@@ -51,12 +44,27 @@
MediaTttLoggerUtils.logPackageNotFound(buffer, TAG, packageName)
}
- fun logRippleAnimationEnd(id: Int) {
+ fun logRippleAnimationEnd(id: Int, type: String) {
buffer.log(
tag,
LogLevel.DEBUG,
- { int1 = id },
- { "ripple animation for view with id: $int1 is ended" }
+ {
+ int1 = id
+ str1 = type
+ },
+ { "ripple animation for view with id=$int1 is ended, animation type=$str1" },
+ )
+ }
+
+ fun logRippleAnimationStart(id: Int, type: String) {
+ buffer.log(
+ tag,
+ LogLevel.DEBUG,
+ {
+ int1 = id
+ str1 = type
+ },
+ { "ripple animation for view with id=$int1 is started, animation type=$str1" },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
index a232971..9d00435 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverRippleController.kt
@@ -69,7 +69,9 @@
)
rippleView.addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
- override fun onViewDetachedFromWindow(view: View) {}
+ override fun onViewDetachedFromWindow(view: View) {
+ view.visibility = View.GONE
+ }
override fun onViewAttachedToWindow(view: View) {
if (view == null) {
@@ -81,7 +83,7 @@
} else {
layoutRipple(attachedRippleView)
}
- attachedRippleView.expandRipple()
+ attachedRippleView.expandRipple(mediaTttReceiverLogger)
attachedRippleView.removeOnAttachStateChangeListener(this)
}
}
@@ -126,7 +128,7 @@
iconRippleView.setMaxSize(radius * 0.8f, radius * 0.8f)
iconRippleView.setCenter(
width * 0.5f,
- height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin()
+ height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin(),
)
iconRippleView.setColor(getRippleColor(), RIPPLE_OPACITY)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 81059e3..cd733ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -37,10 +37,14 @@
isStarted = false
}
- fun expandRipple(onAnimationEnd: Runnable? = null) {
+ fun expandRipple(logger: MediaTttReceiverLogger, onAnimationEnd: Runnable? = null) {
duration = DEFAULT_DURATION
isStarted = true
- super.startRipple(onAnimationEnd)
+ super.startRipple {
+ logger.logRippleAnimationEnd(id, EXPAND)
+ onAnimationEnd?.run()
+ }
+ logger.logRippleAnimationStart(id, EXPAND)
}
/** Used to animate out the ripple. No-op if the ripple was never started via [startRipple]. */
@@ -53,10 +57,14 @@
animator.removeAllListeners()
animator.addListener(
object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation: Animator) {
+ onAnimationEnd(animation)
+ }
+
override fun onAnimationEnd(animation: Animator) {
animation?.let {
visibility = GONE
- logger.logRippleAnimationEnd(id)
+ logger.logRippleAnimationEnd(id, COLLAPSE)
}
onAnimationEnd?.run()
isStarted = false
@@ -64,13 +72,14 @@
}
)
animator.reverse()
+ logger.logRippleAnimationStart(id, COLLAPSE)
}
// Expands the ripple to cover full screen.
fun expandToFull(
newHeight: Float,
logger: MediaTttReceiverLogger,
- onAnimationEnd: Runnable? = null
+ onAnimationEnd: Runnable? = null,
) {
if (!isStarted) {
return
@@ -95,10 +104,14 @@
}
animator.addListener(
object : AnimatorListenerAdapter() {
+ override fun onAnimationCancel(animation: Animator) {
+ onAnimationEnd(animation)
+ }
+
override fun onAnimationEnd(animation: Animator) {
animation?.let {
visibility = GONE
- logger.logRippleAnimationEnd(id)
+ logger.logRippleAnimationEnd(id, EXPAND_TO_FULL)
}
onAnimationEnd?.run()
isStarted = false
@@ -106,6 +119,7 @@
}
)
animator.start()
+ logger.logRippleAnimationStart(id, EXPAND_TO_FULL)
}
// Calculates the actual starting percentage according to ripple shader progress set method.
@@ -151,5 +165,8 @@
companion object {
const val DEFAULT_DURATION = 333L
const val EXPAND_TO_FULL_DURATION = 1000L
+ private const val COLLAPSE = "collapse"
+ private const val EXPAND_TO_FULL = "expand to full"
+ private const val EXPAND = "expand"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index f7a505a..5048a5d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -326,6 +326,11 @@
logGesture(mInRejectedExclusion
? SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED_REJECTED
: SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED);
+ if (!mInRejectedExclusion) {
+ // Log successful back gesture to contextual edu stats
+ mOverviewProxyService.updateContextualEduStats(mIsTrackpadThreeFingerSwipe,
+ GestureType.BACK);
+ }
}
@Override
@@ -1153,8 +1158,6 @@
if (mAllowGesture) {
if (mBackAnimation != null) {
mBackAnimation.onThresholdCrossed();
- mOverviewProxyService.updateContextualEduStats(
- mIsTrackpadThreeFingerSwipe, GestureType.BACK);
} else {
pilferPointers();
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 66ac01a..51d2329 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -52,7 +52,6 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
@@ -62,7 +61,6 @@
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -191,58 +189,22 @@
val context = inflater.context
val composeView =
ComposeView(context).apply {
- setBackPressedDispatcher()
- setContent {
- PlatformTheme {
- val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
-
- AnimatedVisibility(
- visible = visible,
- modifier =
- Modifier.windowInsetsPadding(WindowInsets.navigationBars)
- .thenIf(notificationScrimClippingParams.isEnabled) {
- Modifier.notificationScrimClip(
- notificationScrimClippingParams.leftInset,
- notificationScrimClippingParams.top,
- notificationScrimClippingParams.rightInset,
- notificationScrimClippingParams.bottom,
- notificationScrimClippingParams.radius,
+ repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ setViewTreeOnBackPressedDispatcherOwner(
+ object : OnBackPressedDispatcherOwner {
+ override val onBackPressedDispatcher =
+ OnBackPressedDispatcher().apply {
+ setOnBackInvokedDispatcher(
+ it.viewRootImpl.onBackInvokedDispatcher
)
}
- .graphicsLayer { elevation = 4.dp.toPx() },
- ) {
- val isEditing by
- viewModel.containerViewModel.editModeViewModel.isEditing
- .collectAsStateWithLifecycle()
- val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
- AnimatedContent(
- targetState = isEditing,
- transitionSpec = {
- fadeIn(animationSpecEditMode) togetherWith
- fadeOut(animationSpecEditMode)
- },
- label = "EditModeAnimatedContent",
- ) { editing ->
- if (editing) {
- val qqsPadding by
- viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
- EditMode(
- viewModel = viewModel.containerViewModel.editModeViewModel,
- modifier =
- Modifier.fillMaxWidth()
- .padding(top = { qqsPadding })
- .padding(
- horizontal = {
- QuickSettingsShade.Dimensions.Padding
- .roundToPx()
- }
- ),
- )
- } else {
- CollapsableQuickSettingsSTL()
- }
+
+ override val lifecycle: Lifecycle =
+ this@repeatWhenAttached.lifecycle
}
- }
+ )
+ setContent { this@QSFragmentCompose.Content() }
}
}
}
@@ -261,6 +223,58 @@
return frame
}
+ @Composable
+ private fun Content() {
+ PlatformTheme {
+ val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
+
+ AnimatedVisibility(
+ visible = visible,
+ modifier =
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
+ notificationScrimClippingParams.isEnabled
+ ) {
+ Modifier.notificationScrimClip(
+ notificationScrimClippingParams.leftInset,
+ notificationScrimClippingParams.top,
+ notificationScrimClippingParams.rightInset,
+ notificationScrimClippingParams.bottom,
+ notificationScrimClippingParams.radius,
+ )
+ },
+ ) {
+ val isEditing by
+ viewModel.containerViewModel.editModeViewModel.isEditing
+ .collectAsStateWithLifecycle()
+ val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
+ AnimatedContent(
+ targetState = isEditing,
+ transitionSpec = {
+ fadeIn(animationSpecEditMode) togetherWith fadeOut(animationSpecEditMode)
+ },
+ label = "EditModeAnimatedContent",
+ ) { editing ->
+ if (editing) {
+ val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
+ EditMode(
+ viewModel = viewModel.containerViewModel.editModeViewModel,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = { qqsPadding })
+ .padding(
+ horizontal = {
+ QuickSettingsShade.Dimensions.Padding.roundToPx()
+ }
+ ),
+ )
+ } else {
+ CollapsableQuickSettingsSTL()
+ }
+ }
+ }
+ }
+ }
+
/**
* STL that contains both QQS (tiles) and QS (brightness, tiles, footer actions), but no Edit
* mode. It tracks [QSFragmentComposeViewModel.expansionState] to drive the transition between
@@ -649,23 +663,6 @@
}
}
-private fun View.setBackPressedDispatcher() {
- repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- setViewTreeOnBackPressedDispatcherOwner(
- object : OnBackPressedDispatcherOwner {
- override val onBackPressedDispatcher =
- OnBackPressedDispatcher().apply {
- setOnBackInvokedDispatcher(it.viewRootImpl.onBackInvokedDispatcher)
- }
-
- override val lifecycle: Lifecycle = this@repeatWhenAttached.lifecycle
- }
- )
- }
- }
-}
-
private suspend inline fun <Listener : Any, Data> setListenerJob(
listenerFlow: MutableStateFlow<Listener?>,
dataFlow: Flow<Data>,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
index a4fe4e3..ad76b4f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DndTile.java
@@ -169,50 +169,34 @@
private void enableZenMode(@Nullable Expandable expandable) {
int zenDuration = mSettingZenDuration.getValue();
- boolean showOnboarding = Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0
- && Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.ZEN_SETTINGS_UPDATED, 0) != 1;
- if (showOnboarding) {
- // don't show on-boarding again or notification ever
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
- // turn on DND
- mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
- // show on-boarding screen
- Intent intent = new Intent(Settings.ZEN_MODE_ONBOARDING);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
- } else {
- switch (zenDuration) {
- case Settings.Secure.ZEN_DURATION_PROMPT:
- mUiHandler.post(() -> {
- Dialog dialog = makeZenModeDialog();
- if (expandable != null) {
- DialogTransitionAnimator.Controller controller =
- expandable.dialogTransitionController(new DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG));
- if (controller != null) {
- mDialogTransitionAnimator.show(dialog,
- controller, /* animateBackgroundBoundsChange= */ false);
- } else {
- dialog.show();
- }
+ switch (zenDuration) {
+ case Settings.Secure.ZEN_DURATION_PROMPT:
+ mUiHandler.post(() -> {
+ Dialog dialog = makeZenModeDialog();
+ if (expandable != null) {
+ DialogTransitionAnimator.Controller controller =
+ expandable.dialogTransitionController(new DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG));
+ if (controller != null) {
+ mDialogTransitionAnimator.show(dialog,
+ controller, /* animateBackgroundBoundsChange= */ false);
} else {
dialog.show();
}
- });
- break;
- case Settings.Secure.ZEN_DURATION_FOREVER:
- mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
- break;
- default:
- Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,
- mHost.getUserId(), true).id;
- mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
- conditionId, TAG);
- }
+ } else {
+ dialog.show();
+ }
+ });
+ break;
+ case Settings.Secure.ZEN_DURATION_FOREVER:
+ mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
+ break;
+ default:
+ Uri conditionId = ZenModeConfig.toTimeCondition(mContext, zenDuration,
+ mHost.getUserId(), true).id;
+ mController.setZen(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ conditionId, TAG);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
index 4fdd90b..fb7c34f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -4,10 +4,10 @@
import android.graphics.Bitmap
import android.graphics.Insets
import android.graphics.Rect
-import android.net.Uri
import android.os.Process
import android.os.UserHandle
import android.view.Display
+import android.view.WindowManager
import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.ScreenshotType
import androidx.annotation.VisibleForTesting
@@ -15,22 +15,20 @@
/** [ScreenshotData] represents the current state of a single screenshot being acquired. */
data class ScreenshotData(
- @ScreenshotType var type: Int,
- @ScreenshotSource var source: Int,
+ @ScreenshotType val type: Int,
+ @ScreenshotSource val source: Int,
/** UserHandle for the owner of the app being screenshotted, if known. */
- var userHandle: UserHandle?,
+ val userHandle: UserHandle?,
/** ComponentName of the top-most app in the screenshot. */
- var topComponent: ComponentName?,
+ val topComponent: ComponentName?,
var screenBounds: Rect?,
- var taskId: Int,
+ val taskId: Int,
var insets: Insets,
var bitmap: Bitmap?,
- var displayId: Int,
- /** App-provided URL representing the content the user was looking at in the screenshot. */
- var contextUrl: Uri? = null,
+ val displayId: Int,
) {
- val packageNameString: String
- get() = if (topComponent == null) "" else topComponent!!.packageName
+ val packageNameString
+ get() = topComponent?.packageName ?: ""
fun getUserOrDefault(): UserHandle {
return userHandle ?: Process.myUserHandle()
@@ -52,16 +50,21 @@
)
@VisibleForTesting
- fun forTesting() =
+ fun forTesting(
+ userHandle: UserHandle? = null,
+ source: Int = ScreenshotSource.SCREENSHOT_KEY_CHORD,
+ topComponent: ComponentName? = null,
+ bitmap: Bitmap? = null,
+ ) =
ScreenshotData(
- type = 0,
- source = 0,
- userHandle = null,
- topComponent = null,
+ type = WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ source = source,
+ userHandle = userHandle,
+ topComponent = topComponent,
screenBounds = null,
taskId = 0,
insets = Insets.NONE,
- bitmap = null,
+ bitmap = bitmap,
displayId = Display.DEFAULT_DISPLAY,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 448f7c4..ab8a953 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -20,9 +20,11 @@
import android.os.Trace
import android.util.Log
import android.view.Display
+import android.view.WindowManager.ScreenshotSource
import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
import com.android.internal.logging.UiEventLogger
import com.android.internal.util.ScreenshotRequest
+import com.android.systemui.Flags.screenshotMultidisplayFocusChange
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.data.repository.DisplayRepository
@@ -40,7 +42,7 @@
suspend fun executeScreenshots(
screenshotRequest: ScreenshotRequest,
onSaved: (Uri?) -> Unit,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
)
fun onCloseSystemDialogsReceived()
@@ -52,7 +54,7 @@
fun executeScreenshotsAsync(
screenshotRequest: ScreenshotRequest,
onSaved: Consumer<Uri?>,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
)
}
@@ -60,7 +62,7 @@
fun handleScreenshot(
screenshot: ScreenshotData,
finisher: Consumer<Uri?>,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
)
}
@@ -75,7 +77,7 @@
@Inject
constructor(
private val interactiveScreenshotHandlerFactory: InteractiveScreenshotHandler.Factory,
- displayRepository: DisplayRepository,
+ private val displayRepository: DisplayRepository,
@Application private val mainScope: CoroutineScope,
private val screenshotRequestProcessor: ScreenshotRequestProcessor,
private val uiEventLogger: UiEventLogger,
@@ -95,31 +97,44 @@
override suspend fun executeScreenshots(
screenshotRequest: ScreenshotRequest,
onSaved: (Uri?) -> Unit,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
) {
- val displays = getDisplaysToScreenshot(screenshotRequest.type)
- val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
- if (displays.isEmpty()) {
- Log.wtf(TAG, "No displays found for screenshot.")
- }
- displays.forEach { display ->
- val displayId = display.displayId
- var screenshotHandler: ScreenshotHandler =
- if (displayId == Display.DEFAULT_DISPLAY) {
- getScreenshotController(display)
- } else {
- headlessScreenshotHandler
- }
- Log.d(TAG, "Executing screenshot for display $displayId")
+ if (screenshotMultidisplayFocusChange()) {
+ val display = getDisplayToScreenshot(screenshotRequest)
+ val screenshotHandler = getScreenshotController(display)
dispatchToController(
screenshotHandler,
- rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
- onSaved =
- if (displayId == Display.DEFAULT_DISPLAY) {
- onSaved
- } else { _ -> },
- callback = resultCallbackWrapper.createCallbackForId(displayId)
+ ScreenshotData.fromRequest(screenshotRequest, display.displayId),
+ onSaved,
+ requestCallback,
)
+ } else {
+ val displays = getDisplaysToScreenshot(screenshotRequest.type)
+ val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
+ if (displays.isEmpty()) {
+ Log.e(TAG, "No displays found for screenshot.")
+ }
+
+ displays.forEach { display ->
+ val displayId = display.displayId
+ var screenshotHandler: ScreenshotHandler =
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ getScreenshotController(display)
+ } else {
+ headlessScreenshotHandler
+ }
+
+ Log.d(TAG, "Executing screenshot for display $displayId")
+ dispatchToController(
+ screenshotHandler,
+ rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
+ onSaved =
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ onSaved
+ } else { _ -> },
+ callback = resultCallbackWrapper.createCallbackForId(displayId),
+ )
+ }
}
}
@@ -128,7 +143,7 @@
screenshotHandler: ScreenshotHandler,
rawScreenshotData: ScreenshotData,
onSaved: (Uri?) -> Unit,
- callback: RequestCallback
+ callback: RequestCallback,
) {
// Let's wait before logging "screenshot requested", as we should log the processed
// ScreenshotData.
@@ -160,13 +175,13 @@
uiEventLogger.log(
ScreenshotEvent.getScreenshotSource(screenshotData.source),
0,
- screenshotData.packageNameString
+ screenshotData.packageNameString,
)
}
private fun onFailedScreenshotRequest(
screenshotData: ScreenshotData,
- callback: RequestCallback
+ callback: RequestCallback,
) {
uiEventLogger.log(SCREENSHOT_CAPTURE_FAILED, 0, screenshotData.packageNameString)
getNotificationController(screenshotData.displayId)
@@ -184,6 +199,31 @@
}
}
+ // Return the single display to be screenshot based upon the request.
+ private suspend fun getDisplayToScreenshot(screenshotRequest: ScreenshotRequest): Display {
+ return when (screenshotRequest.source) {
+ ScreenshotSource.SCREENSHOT_OVERVIEW ->
+ // Show on the display where overview was shown if available.
+ displayRepository.getDisplay(screenshotRequest.displayId)
+ ?: displayRepository.getDisplay(Display.DEFAULT_DISPLAY)
+ ?: error("Can't find default display")
+
+ // Key chord and vendor gesture occur on the device itself, so screenshot the device's
+ // display
+ ScreenshotSource.SCREENSHOT_KEY_CHORD,
+ ScreenshotSource.SCREENSHOT_VENDOR_GESTURE ->
+ displayRepository.getDisplay(Display.DEFAULT_DISPLAY)
+ ?: error("Can't find default display")
+
+ // All other invocations use the focused display
+ else -> focusedDisplay()
+ }
+ }
+
+ // TODO(b/367394043): Determine the focused display here.
+ private suspend fun focusedDisplay() =
+ displayRepository.getDisplay(Display.DEFAULT_DISPLAY) ?: error("Can't find default display")
+
/** Propagates the close system dialog signal to the ScreenshotController. */
override fun onCloseSystemDialogsReceived() {
if (screenshotController?.isPendingSharedTransition() == false) {
@@ -214,7 +254,7 @@
override fun executeScreenshotsAsync(
screenshotRequest: ScreenshotRequest,
onSaved: Consumer<Uri?>,
- requestCallback: RequestCallback
+ requestCallback: RequestCallback,
) {
mainScope.launch {
executeScreenshots(screenshotRequest, { uri -> onSaved.accept(uri) }, requestCallback)
@@ -235,9 +275,7 @@
* - If any finished with an error, [reportError] of [originalCallback] is called
* - Otherwise, [onFinish] is called.
*/
- private class MultiResultCallbackWrapper(
- private val originalCallback: RequestCallback,
- ) {
+ private class MultiResultCallbackWrapper(private val originalCallback: RequestCallback) {
private val idsPending = mutableSetOf<Int>()
private val idsWithErrors = mutableSetOf<Int>()
@@ -290,7 +328,7 @@
Display.TYPE_EXTERNAL,
Display.TYPE_INTERNAL,
Display.TYPE_OVERLAY,
- Display.TYPE_WIFI
+ Display.TYPE_WIFI,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4f47536..f83548d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -994,7 +994,9 @@
// be dropped, causing the shade expansion to fail silently. Since the shade doesn't open,
// it doesn't become visible, and the bounds will never update. Therefore, we must detect
// the incorrect bounds here and force the update so that touches are routed correctly.
- if (SceneContainerFlag.isEnabled() && mWindowRootView.getVisibility() == View.INVISIBLE) {
+ if (SceneContainerFlag.isEnabled()
+ && mWindowRootView != null
+ && mWindowRootView.getVisibility() == View.INVISIBLE) {
Rect bounds = newConfig.windowConfiguration.getBounds();
if (mWindowRootView.getWidth() != bounds.width()) {
mLogger.logConfigChangeWidthAdjust(mWindowRootView.getWidth(), bounds.width());
diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index bbcf10b..5b82772 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -13,10 +13,9 @@
per-file *Repository* = set noparent
per-file *Repository* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com
-per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
-per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
+per-file NotificationShadeWindow* = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com
per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com
-per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
-per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com
+per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com
+per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com, justinweir@google.com, syeonlee@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
index cc6e8c2..3113dc4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeUserActionsViewModel.kt
@@ -32,6 +32,7 @@
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
/**
* Models the UI state for the user actions that the user can perform to navigate to other scenes.
@@ -50,7 +51,9 @@
combine(
shadeInteractor.shadeMode,
qsSceneAdapter.isCustomizerShowing,
- sceneBackInteractor.backScene.map { it ?: SceneFamilies.Home },
+ sceneBackInteractor.backScene
+ .filter { it != Scenes.Shade }
+ .map { it ?: SceneFamilies.Home },
) { shadeMode, isCustomizerShowing, backScene ->
buildMap<UserAction, UserActionResult> {
if (!isCustomizerShowing) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 7244f8a..e47952f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -62,11 +62,13 @@
import com.android.systemui.dagger.SysUISingleton;
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.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.recents.OverviewProxyService;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -286,6 +288,8 @@
protected ContentObserver mLockscreenSettingsObserver;
protected ContentObserver mSettingsObserver;
+ private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+
@Inject
public NotificationLockscreenUserManagerImpl(Context context,
BroadcastDispatcher broadcastDispatcher,
@@ -305,7 +309,8 @@
SecureSettings secureSettings,
DumpManager dumpManager,
LockPatternUtils lockPatternUtils,
- FeatureFlagsClassic featureFlags) {
+ FeatureFlagsClassic featureFlags,
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy) {
mContext = context;
mMainExecutor = mainExecutor;
mBackgroundExecutor = backgroundExecutor;
@@ -325,6 +330,7 @@
mSecureSettings = secureSettings;
mKeyguardStateController = keyguardStateController;
mFeatureFlags = featureFlags;
+ mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
mLockScreenUris.add(SHOW_LOCKSCREEN);
mLockScreenUris.add(SHOW_PRIVATE_LOCKSCREEN);
@@ -748,8 +754,13 @@
// camera on the keyguard has a state of SHADE but the keyguard is still showing.
final boolean showingKeyguard = mState != StatusBarState.SHADE
|| mKeyguardStateController.isShowing();
- final boolean devicePublic = showingKeyguard && mKeyguardStateController.isMethodSecure();
-
+ final boolean devicePublic;
+ if (SceneContainerFlag.isEnabled()) {
+ devicePublic = !mDeviceUnlockedInteractorLazy.get()
+ .getDeviceUnlockStatus().getValue().isUnlocked();
+ } else {
+ devicePublic = showingKeyguard && mKeyguardStateController.isMethodSecure();
+ }
// Look for public mode users. Users are considered public in either case of:
// - device keyguard is shown in secure mode;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 5d14be8..73ad0e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -272,7 +272,7 @@
* Updates the {@link StatusBarState} and notifies registered listeners, if needed.
*/
private void updateStateAndNotifyListeners(int state) {
- if (state != mUpcomingState) {
+ if (state != mUpcomingState && !SceneContainerFlag.isEnabled()) {
Log.d(TAG, "setState: requested state " + StatusBarState.toString(state)
+ "!= upcomingState: " + StatusBarState.toString(mUpcomingState) + ". "
+ "This usually means the status bar state transition was interrupted before "
@@ -728,20 +728,23 @@
// doesn't work well for clients of this class (like remote input) that expect the device to
// be fully and properly unlocked when the state changes to SHADE.
//
- // Therefore, we calculate the device to be in a locked-ish state (KEYGUARD or SHADE_LOCKED,
+ // Therefore, we consider the device to be in a keyguardish state (KEYGUARD or SHADE_LOCKED,
// but not SHADE) if *any* of these are still true:
// 1. deviceUnlockStatus.isUnlocked is false.
- // 2. We are on (currentScene equals) a locked-ish scene (Lockscreen, Bouncer, or Communal).
- // 3. We are over (backStack contains) a locked-ish scene (Lockscreen or Communal).
+ // 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal).
+ // 3. backStack contains a keyguardish scene (Lockscreen or Communal).
+
+ final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal;
+ final boolean overKeyguardish = overLockscreen || overCommunal;
if (isOccluded) {
// Occlusion is special; even though the device is still technically on the lockscreen,
// the UI behaves as if it is unlocked.
newState = StatusBarState.SHADE;
- } else if (onLockscreen || onBouncer || onCommunal || overLockscreen || overCommunal) {
- // We get here if we are on or over a locked-ish scene, even if isUnlocked is true; we
+ } else if (onKeyguardish || overKeyguardish) {
+ // We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we
// want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a
- // locked-ish scene.
+ // keyguardish scene.
if (onShade || onQuickSettings || overShade || overlaidShade || overlaidQuickSettings) {
newState = StatusBarState.SHADE_LOCKED;
} else {
@@ -751,7 +754,7 @@
newState = StatusBarState.SHADE;
} else if (onShade || onQuickSettings) {
// We get here if deviceUnlockStatus.isUnlocked is false but we are no longer on or over
- // a locked-ish scene; we want to return SHADE_LOCKED until isUnlocked is also true.
+ // a keyguardish scene; we want to return SHADE_LOCKED until isUnlocked is also true.
newState = StatusBarState.SHADE_LOCKED;
} else {
throw new IllegalArgumentException(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 97add30..a24f267 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -91,7 +91,6 @@
constructor(
private val context: Context,
private val featureFlags: FeatureFlags,
- private val smartspaceManager: SmartspaceManager?,
private val activityStarter: ActivityStarter,
private val falsingManager: FalsingManager,
private val systemClock: SystemClock,
@@ -124,6 +123,7 @@
private const val MAX_RECENT_SMARTSPACE_DATA_FOR_DUMP = 5
}
+ private var userSmartspaceManager: SmartspaceManager? = null
private var session: SmartspaceSession? = null
private val datePlugin: BcSmartspaceDataPlugin? = optionalDatePlugin.orElse(null)
private val weatherPlugin: BcSmartspaceDataPlugin? = optionalWeatherPlugin.orElse(null)
@@ -461,7 +461,11 @@
}
private fun connectSession() {
- if (smartspaceManager == null) return
+ if (userSmartspaceManager == null) {
+ userSmartspaceManager =
+ userTracker.userContext.getSystemService(SmartspaceManager::class.java)
+ }
+ if (userSmartspaceManager == null) return
if (datePlugin == null && weatherPlugin == null && plugin == null) return
if (session != null || smartspaceViews.isEmpty()) {
return
@@ -474,12 +478,14 @@
return
}
- val newSession = smartspaceManager.createSmartspaceSession(
- SmartspaceConfig.Builder(
- context, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD).build())
+ val newSession = userSmartspaceManager?.createSmartspaceSession(
+ SmartspaceConfig.Builder(
+ userTracker.userContext, BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD
+ ).build()
+ )
Log.d(TAG, "Starting smartspace session for " +
BcSmartspaceDataPlugin.UI_SURFACE_LOCK_SCREEN_AOD)
- newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
+ newSession?.addOnTargetsAvailableListener(uiExecutor, sessionListener)
this.session = newSession
deviceProvisionedController.removeCallback(deviceProvisionedListener)
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 5f4f72f..0474344 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -594,7 +594,9 @@
private final ColorExtractor.OnColorsChangedListener mOnColorsChangedListener =
(extractor, which) -> updateTheme();
private final BrightnessMirrorShowingInteractor mBrightnessMirrorShowingInteractor;
- private final GlanceableHubContainerController mGlanceableHubContainerController;
+
+ // Only use before the scene container. Null if scene container is enabled.
+ @Nullable private final GlanceableHubContainerController mGlanceableHubContainerController;
private final EmergencyGestureIntentFactory mEmergencyGestureIntentFactory;
@@ -807,7 +809,11 @@
mFingerprintManager = fingerprintManager;
mActivityStarter = activityStarter;
mBrightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor;
- mGlanceableHubContainerController = glanceableHubContainerController;
+ if (!SceneContainerFlag.isEnabled()) {
+ mGlanceableHubContainerController = glanceableHubContainerController;
+ } else {
+ mGlanceableHubContainerController = null;
+ }
mEmergencyGestureIntentFactory = emergencyGestureIntentFactory;
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
@@ -2972,7 +2978,9 @@
@Override
public void handleCommunalHubTouch(MotionEvent event) {
- mGlanceableHubContainerController.onTouchEvent(event);
+ if (mGlanceableHubContainerController != null) {
+ mGlanceableHubContainerController.onTouchEvent(event);
+ }
}
@Override
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 1ea26e5..5ae24f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -63,6 +63,7 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
import com.android.systemui.bouncer.ui.BouncerView;
+import com.android.systemui.bouncer.util.BouncerTestUtilsKt;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
@@ -1552,8 +1553,10 @@
}
public boolean shouldDismissOnMenuPressed() {
- return mPrimaryBouncerView.getDelegate() != null
- && mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed();
+ return (mPrimaryBouncerView.getDelegate() != null
+ && mPrimaryBouncerView.getDelegate().shouldDismissOnMenuPressed()) || (
+ ComposeBouncerFlags.INSTANCE.isEnabled() && BouncerTestUtilsKt.shouldEnableMenuKey(
+ mContext.getResources()));
}
public boolean interceptMediaKey(KeyEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
index b1754fd..200f080 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallback.java
@@ -34,11 +34,15 @@
import androidx.annotation.Nullable;
+import com.android.compose.animation.scene.ObservableTransitionState;
import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.ActionClickLogger;
import com.android.systemui.statusbar.CommandQueue;
@@ -52,6 +56,9 @@
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.kotlin.JavaAdapter;
+
+import dagger.Lazy;
import java.util.concurrent.Executor;
@@ -80,6 +87,8 @@
private final ActionClickLogger mActionClickLogger;
private int mDisabled2;
protected BroadcastReceiver mChallengeReceiver = new ChallengeReceiver();
+ private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+ private final Lazy<SceneInteractor> mSceneInteractorLazy;
/**
*/
@@ -95,7 +104,10 @@
ShadeController shadeController,
CommandQueue commandQueue,
ActionClickLogger clickLogger,
- @Main Executor executor) {
+ @Main Executor executor,
+ Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
+ Lazy<SceneInteractor> sceneInteractorLazy,
+ JavaAdapter javaAdapter) {
mContext = context;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mShadeController = shadeController;
@@ -113,20 +125,28 @@
mActionClickLogger = clickLogger;
mActivityIntentHelper = new ActivityIntentHelper(mContext);
mGroupExpansionManager = groupExpansionManager;
+ mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
+ mSceneInteractorLazy = sceneInteractorLazy;
+
+ if (SceneContainerFlag.isEnabled()) {
+ javaAdapter.alwaysCollectFlow(
+ mDeviceUnlockedInteractorLazy.get().getDeviceUnlockStatus(),
+ deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState()));
+ javaAdapter.alwaysCollectFlow(
+ mSceneInteractorLazy.get().getTransitionState(),
+ deviceUnlockStatus -> onStateChanged(mStatusBarStateController.getState()));
+ }
}
@Override
public void onStateChanged(int state) {
- boolean hasPendingRemoteInput = mPendingRemoteInputView != null;
- if (state == StatusBarState.SHADE
- && (mStatusBarStateController.leaveOpenOnKeyguardHide() || hasPendingRemoteInput)) {
- if (!mStatusBarStateController.isKeyguardRequested()
- && mKeyguardStateController.isUnlocked()) {
- if (hasPendingRemoteInput) {
- mExecutor.execute(mPendingRemoteInputView::callOnClick);
- }
- mPendingRemoteInputView = null;
- }
+ if (mPendingRemoteInputView == null) {
+ return;
+ }
+
+ if (state == StatusBarState.SHADE && canRetryPendingRemoteInput()) {
+ mExecutor.execute(mPendingRemoteInputView::callOnClick);
+ mPendingRemoteInputView = null;
}
}
@@ -320,6 +340,23 @@
}
}
+ /**
+ * Returns {@code true} if it is safe to retry a pending remote input. The exact criteria for
+ * this vary depending whether the scene container is enabled.
+ */
+ private boolean canRetryPendingRemoteInput() {
+ if (SceneContainerFlag.isEnabled()) {
+ final boolean isUnlocked = mDeviceUnlockedInteractorLazy.get()
+ .getDeviceUnlockStatus().getValue().isUnlocked();
+ final boolean isIdle = mSceneInteractorLazy.get()
+ .getTransitionState().getValue() instanceof ObservableTransitionState.Idle;
+ return isUnlocked && isIdle;
+ } else {
+ return mKeyguardStateController.isUnlocked()
+ && !mStatusBarStateController.isKeyguardRequested();
+ }
+ }
+
protected class ChallengeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
index da928a3..3cf2066 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java
@@ -32,6 +32,7 @@
import androidx.annotation.NonNull;
import com.android.internal.camera.flags.Flags;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.ListenerSet;
import java.util.Set;
@@ -41,14 +42,17 @@
private static final int[] SENSORS = new int[] {CAMERA, MICROPHONE};
private final @NonNull SensorPrivacyManager mSensorPrivacyManager;
+ private final @NonNull UserTracker mUserTracker;
private final SparseBooleanArray mSoftwareToggleState = new SparseBooleanArray();
private final SparseBooleanArray mHardwareToggleState = new SparseBooleanArray();
private Boolean mRequiresAuthentication;
private final ListenerSet<Callback> mCallbacks = new ListenerSet<>();
public IndividualSensorPrivacyControllerImpl(
- @NonNull SensorPrivacyManager sensorPrivacyManager) {
+ @NonNull SensorPrivacyManager sensorPrivacyManager,
+ @NonNull UserTracker userTracker) {
mSensorPrivacyManager = sensorPrivacyManager;
+ mUserTracker = userTracker;
}
@Override
@@ -94,12 +98,14 @@
@Override
public void setSensorBlocked(@Source int source, @Sensor int sensor, boolean blocked) {
- mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked);
+ mSensorPrivacyManager.setSensorPrivacyForProfileGroup(source, sensor, blocked,
+ mUserTracker.getUserId());
}
@Override
public void suppressSensorPrivacyReminders(int sensor, boolean suppress) {
- mSensorPrivacyManager.suppressSensorPrivacyReminders(sensor, suppress);
+ mSensorPrivacyManager.suppressSensorPrivacyReminders(sensor, suppress,
+ mUserTracker.getUserId());
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 7c055c8..7f90242 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -135,3 +135,15 @@
): Flow<R> {
return combine(flow, flow2, flow3, flow4, flow5, transform)
}
+
+fun <T1, T2, T3, T4, T5, T6, R> combineFlows(
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ transform: (T1, T2, T3, T4, T5, T6) -> R,
+): Flow<R> {
+ return combine(flow, flow2, flow3, flow4, flow5, flow6, transform)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 079c72f..1f92bc1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -37,11 +37,8 @@
import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.IVolumeController;
-import android.media.MediaRoute2Info;
import android.media.MediaRouter2Manager;
-import android.media.RoutingSessionInfo;
import android.media.VolumePolicy;
-import android.media.session.MediaController;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession.Token;
import android.net.Uri;
@@ -88,7 +85,6 @@
import java.io.PrintWriter;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@@ -217,7 +213,7 @@
VolumeDialogControllerImpl.class.getSimpleName());
mWorker = new W(mWorkerLooper);
mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
- mMediaSessionsCallbacksW = new MediaSessionsCallbacks(mContext);
+ mMediaSessionsCallbacksW = new MediaSessionsCallbacks();
mMediaSessions = createMediaSessions(mContext, mWorkerLooper, mMediaSessionsCallbacksW);
mAudioSharingInteractor = audioSharingInteractor;
mJavaAdapter = javaAdapter;
@@ -1360,16 +1356,9 @@
private final HashMap<Token, Integer> mRemoteStreams = new HashMap<>();
private int mNextStream = DYNAMIC_STREAM_REMOTE_START_INDEX;
- private final boolean mVolumeAdjustmentForRemoteGroupSessions;
-
- public MediaSessionsCallbacks(Context context) {
- mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
- }
@Override
public void onRemoteUpdate(Token token, String name, PlaybackInfo pi) {
- if (showForSession(token)) {
addStream(token, "onRemoteUpdate");
int stream = 0;
@@ -1396,12 +1385,10 @@
Log.d(TAG, "onRemoteUpdate: " + name + ": " + ss.level + " of " + ss.levelMax);
mCallbacks.onStateChanged(mState);
}
- }
}
@Override
public void onRemoteVolumeChanged(Token token, int flags) {
- if (showForSession(token)) {
addStream(token, "onRemoteVolumeChanged");
int stream = 0;
synchronized (mRemoteStreams) {
@@ -1420,27 +1407,27 @@
if (showUI) {
onShowRequestedW(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED);
}
- }
}
@Override
public void onRemoteRemoved(Token token) {
- if (showForSession(token)) {
- int stream = 0;
- synchronized (mRemoteStreams) {
- if (!mRemoteStreams.containsKey(token)) {
- Log.d(TAG, "onRemoteRemoved: stream doesn't exist, "
- + "aborting remote removed for token:" + token.toString());
- return;
- }
- stream = mRemoteStreams.get(token);
+ int stream;
+ synchronized (mRemoteStreams) {
+ if (!mRemoteStreams.containsKey(token)) {
+ Log.d(
+ TAG,
+ "onRemoteRemoved: stream doesn't exist, "
+ + "aborting remote removed for token:"
+ + token.toString());
+ return;
}
- mState.states.remove(stream);
- if (mState.activeStream == stream) {
- updateActiveStreamW(-1);
- }
- mCallbacks.onStateChanged(mState);
+ stream = mRemoteStreams.get(token);
}
+ mState.states.remove(stream);
+ if (mState.activeStream == stream) {
+ updateActiveStreamW(-1);
+ }
+ mCallbacks.onStateChanged(mState);
}
public void setStreamVolume(int stream, int level) {
@@ -1449,39 +1436,7 @@
Log.w(TAG, "setStreamVolume: No token found for stream: " + stream);
return;
}
- if (showForSession(token)) {
- mMediaSessions.setVolume(token, level);
- }
- }
-
- private boolean showForSession(Token token) {
- if (mVolumeAdjustmentForRemoteGroupSessions) {
- if (DEBUG) {
- Log.d(TAG, "Volume adjustment for remote group sessions allowed,"
- + " showForSession: true");
- }
- return true;
- }
- MediaController ctr = new MediaController(mContext, token);
- String packageName = ctr.getPackageName();
- List<RoutingSessionInfo> sessions =
- mRouter2Manager.getRoutingSessions(packageName);
- if (DEBUG) {
- Log.d(TAG, "Found " + sessions.size() + " routing sessions for package name "
- + packageName);
- }
- for (RoutingSessionInfo session : sessions) {
- if (DEBUG) {
- Log.d(TAG, "Found routingSessionInfo: " + session);
- }
- if (!session.isSystemSession()
- && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
- return true;
- }
- }
-
- Log.d(TAG, "No routing session for " + packageName);
- return false;
+ mMediaSessions.setVolume(token, level);
}
private Token findToken(int stream) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index ebb9ce9..ed8de69 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -23,6 +23,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogManager;
import com.android.systemui.plugins.VolumeDialog;
@@ -40,6 +41,8 @@
import com.android.systemui.volume.VolumeDialogImpl;
import com.android.systemui.volume.VolumePanelDialogReceiver;
import com.android.systemui.volume.VolumeUI;
+import com.android.systemui.volume.dialog.VolumeDialogPlugin;
+import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent;
import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor;
import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
import com.android.systemui.volume.panel.dagger.VolumePanelComponent;
@@ -66,7 +69,8 @@
SpatializerModule.class,
},
subcomponents = {
- VolumePanelComponent.class
+ VolumePanelComponent.class,
+ VolumeDialogPluginComponent.class,
}
)
public interface VolumeModule {
@@ -101,6 +105,7 @@
/** */
@Provides
static VolumeDialog provideVolumeDialog(
+ Lazy<VolumeDialogPlugin> volumeDialogProvider,
Context context,
VolumeDialogController volumeDialogController,
AccessibilityManagerWrapper accessibilityManagerWrapper,
@@ -118,29 +123,33 @@
VibratorHelper vibratorHelper,
SystemClock systemClock,
VolumeDialogInteractor interactor) {
- VolumeDialogImpl impl = new VolumeDialogImpl(
- context,
- volumeDialogController,
- accessibilityManagerWrapper,
- deviceProvisionedController,
- configurationController,
- mediaOutputDialogManager,
- interactionJankMonitor,
- volumePanelNavigationInteractor,
- volumeNavigator,
- true, /* should listen for jank */
- csdFactory,
- devicePostureController,
- Looper.getMainLooper(),
- volumePanelFlag,
- dumpManager,
- secureSettings,
- vibratorHelper,
- systemClock,
- interactor);
- impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
- impl.setAutomute(true);
- impl.setSilentMode(false);
- return impl;
+ if (Flags.volumeRedesign()) {
+ return volumeDialogProvider.get();
+ } else {
+ VolumeDialogImpl impl = new VolumeDialogImpl(
+ context,
+ volumeDialogController,
+ accessibilityManagerWrapper,
+ deviceProvisionedController,
+ configurationController,
+ mediaOutputDialogManager,
+ interactionJankMonitor,
+ volumePanelNavigationInteractor,
+ volumeNavigator,
+ true, /* should listen for jank */
+ csdFactory,
+ devicePostureController,
+ Looper.getMainLooper(),
+ volumePanelFlag,
+ dumpManager,
+ secureSettings,
+ vibratorHelper,
+ systemClock,
+ interactor);
+ impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
+ impl.setAutomute(true);
+ impl.setSilentMode(false);
+ return impl;
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
new file mode 100644
index 0000000..869b3c6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.volume.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class NewVolumeDialog @Inject constructor(@Application context: Context) :
+ Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.volume_dialog)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
new file mode 100644
index 0000000..b93714a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.volume.dialog
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+class NewVolumeDialogPlugin
+@Inject
+constructor(
+ @Application private val applicationCoroutineScope: CoroutineScope,
+ private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
+) : VolumeDialog {
+
+ private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
+ private var job: Job? = null
+
+ override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
+ job =
+ applicationCoroutineScope.launch {
+ coroutineScope {
+ volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
+ }
+ }
+ }
+
+ private fun showDialog() {
+ val volumeDialogPluginComponent =
+ volumeDialogPluginComponent ?: error("Creating dialog before init was called")
+ volumeDialogPluginComponent.coroutineScope().launch {
+ coroutineScope {
+ val volumeDialogComponent: VolumeDialogComponent =
+ volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
+ with(volumeDialogComponent.volumeDialog()) {
+ setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
+ show()
+ }
+ }
+ }
+ }
+
+ override fun destroy() {
+ job?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
new file mode 100644
index 0000000..74e823e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.volume.dialog
+
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.view.ContextThemeWrapper
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class VolumeDialog @Inject constructor(@Application context: Context) :
+ Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.volume_dialog)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
new file mode 100644
index 0000000..a2e81d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.volume.dialog
+
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+class VolumeDialogPlugin
+@Inject
+constructor(
+ @Application private val applicationCoroutineScope: CoroutineScope,
+ private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
+) : VolumeDialog {
+
+ private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
+ private var job: Job? = null
+
+ override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
+ job =
+ applicationCoroutineScope.launch {
+ coroutineScope {
+ volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
+ }
+ }
+ }
+
+ private fun showDialog() {
+ val volumeDialogPluginComponent =
+ volumeDialogPluginComponent ?: error("Creating dialog before init was called")
+ volumeDialogPluginComponent.coroutineScope().launch {
+ coroutineScope {
+ val volumeDialogComponent: VolumeDialogComponent =
+ volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
+ with(volumeDialogComponent.volumeDialog()) {
+ setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
+ show()
+ }
+ }
+ }
+ }
+
+ override fun destroy() {
+ job?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
new file mode 100644
index 0000000..f7ad320
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponent.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.volume.dialog.dagger
+
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import dagger.BindsInstance
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Core Volume Dialog dagger component. It's managed by
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] and lives alongside it.
+ */
+@VolumeDialogScope
+@Subcomponent(modules = [])
+interface VolumeDialogComponent {
+
+ /**
+ * Provides a coroutine scope to use inside [VolumeDialogScope].
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
+ * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
+ * dialog is not shown.
+ */
+ @VolumeDialog fun coroutineScope(): CoroutineScope
+
+ @VolumeDialogScope fun volumeDialog(): com.android.systemui.volume.dialog.VolumeDialog
+
+ @Subcomponent.Factory
+ interface Factory {
+
+ fun create(@BindsInstance @VolumeDialog scope: CoroutineScope): VolumeDialogComponent
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
new file mode 100644
index 0000000..82612a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.volume.dialog.dagger
+
+import com.android.systemui.volume.dialog.dagger.module.VolumeDialogPluginModule
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import dagger.BindsInstance
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Volume Dialog plugin dagger component. It's managed by
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] and lives alongside it.
+ */
+@VolumeDialogPluginScope
+@Subcomponent(modules = [VolumeDialogPluginModule::class])
+interface VolumeDialogPluginComponent {
+
+ /**
+ * Provides a coroutine scope to use inside [VolumeDialogPluginScope].
+ * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
+ * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
+ * dialog is not shown.
+ */
+ @VolumeDialogPlugin fun coroutineScope(): CoroutineScope
+
+ fun volumeDialogComponentFactory(): VolumeDialogComponent.Factory
+
+ @Subcomponent.Factory
+ interface Factory {
+
+ fun create(
+ @BindsInstance @VolumeDialogPlugin scope: CoroutineScope
+ ): VolumeDialogPluginComponent
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
new file mode 100644
index 0000000..3fdf86a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogPluginModule.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.dagger.module
+
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import dagger.Module
+
+@Module(subcomponents = [VolumeDialogComponent::class]) interface VolumeDialogPluginModule
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialog.kt
new file mode 100644
index 0000000..34bddb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialog.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.volume.dialog.dagger.scope
+
+import javax.inject.Qualifier
+
+/**
+ * Volume Dialog qualifier.
+ *
+ * @see VolumeDialogScope
+ */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class VolumeDialog
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPlugin.kt
new file mode 100644
index 0000000..1038c30
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPlugin.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.dagger.scope
+
+import javax.inject.Qualifier
+
+/**
+ * Volume Dialog plugin qualifier.
+ *
+ * @see VolumeDialogPluginScope
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class VolumeDialogPlugin
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPluginScope.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPluginScope.kt
new file mode 100644
index 0000000..6c5f672
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogPluginScope.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.dagger.scope
+
+import javax.inject.Scope
+
+/**
+ * Volume Dialog plugin dependency injection scope. This scope is created alongside Volume Dialog
+ * plugin is initialized and destroyed alongside it. This is effectively almost similar
+ * to @Application now.
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+@Scope
+annotation class VolumeDialogPluginScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogScope.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogScope.kt
new file mode 100644
index 0000000..52caa6a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/scope/VolumeDialogScope.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.volume.dialog.dagger.scope
+
+import javax.inject.Scope
+
+/**
+ * Volume Panel dependency injection scope. This scope is created alongside Volume Panel and
+ * destroyed when it's lo longer present.
+ */
+@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class VolumeDialogScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
new file mode 100644
index 0000000..ec7c6ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.annotation.SuppressLint
+import android.os.Handler
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.shareIn
+
+private const val BUFFER_CAPACITY = 16
+
+/**
+ * Exposes [VolumeDialogController] callback events in the [event].
+ *
+ * @see VolumeDialogController.Callbacks
+ */
+@VolumeDialog
+class VolumeDialogCallbacksInteractor
+@Inject
+constructor(
+ private val volumeDialogController: VolumeDialogController,
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+ @Background private val bgHandler: Handler,
+) {
+
+ @SuppressLint("SharedFlowCreation") // event-but needed
+ val event: Flow<VolumeDialogEventModel> =
+ callbackFlow {
+ val producer = VolumeDialogEventModelProducer(this)
+ volumeDialogController.addCallback(producer, bgHandler)
+ awaitClose { volumeDialogController.removeCallback(producer) }
+ }
+ .buffer(BUFFER_CAPACITY)
+ .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.WhileSubscribed())
+
+ private class VolumeDialogEventModelProducer(
+ private val scope: ProducerScope<VolumeDialogEventModel>
+ ) : VolumeDialogController.Callbacks {
+ override fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) {
+ scope.trySend(
+ VolumeDialogEventModel.ShowRequested(
+ reason = reason,
+ keyguardLocked = keyguardLocked,
+ lockTaskModeState = lockTaskModeState,
+ )
+ )
+ }
+
+ override fun onDismissRequested(reason: Int) {
+ scope.trySend(VolumeDialogEventModel.DismissRequested(reason))
+ }
+
+ override fun onStateChanged(state: VolumeDialogController.State?) {
+ if (state != null) {
+ scope.trySend(VolumeDialogEventModel.StateChanged(VolumeDialogStateModel(state)))
+ }
+ }
+
+ override fun onLayoutDirectionChanged(layoutDirection: Int) {
+ scope.trySend(VolumeDialogEventModel.LayoutDirectionChanged(layoutDirection))
+ }
+
+ // Configuration change is never emitted by the VolumeDialogControllerImpl now.
+ override fun onConfigurationChanged() = Unit
+
+ override fun onShowVibrateHint() {
+ scope.trySend(VolumeDialogEventModel.ShowVibrateHint)
+ }
+
+ override fun onShowSilentHint() {
+ scope.trySend(VolumeDialogEventModel.ShowSilentHint)
+ }
+
+ override fun onScreenOff() {
+ scope.trySend(VolumeDialogEventModel.ScreenOff)
+ }
+
+ override fun onShowSafetyWarning(flags: Int) {
+ scope.trySend(VolumeDialogEventModel.ShowSafetyWarning(flags))
+ }
+
+ override fun onAccessibilityModeChanged(showA11yStream: Boolean) {
+ scope.trySend(VolumeDialogEventModel.AccessibilityModeChanged(showA11yStream))
+ }
+
+ // Captions button is remove from the Volume Dialog
+ override fun onCaptionComponentStateChanged(
+ isComponentEnabled: Boolean,
+ fromTooltip: Boolean,
+ ) = Unit
+
+ // Captions button is remove from the Volume Dialog
+ override fun onCaptionEnabledStateChanged(isEnabled: Boolean, checkBeforeSwitch: Boolean) =
+ Unit
+
+ override fun onShowCsdWarning(csdWarning: Int, durationMs: Int) {
+ scope.trySend(
+ VolumeDialogEventModel.ShowCsdWarning(
+ csdWarning = csdWarning,
+ durationMs = durationMs,
+ )
+ )
+ }
+
+ override fun onVolumeChangedFromKey() {
+ scope.trySend(VolumeDialogEventModel.VolumeChangedFromKey)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
new file mode 100644
index 0000000..dd51108
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Exposes [VolumeDialogController.getState] in the [volumeDialogState].
+ *
+ * @see [VolumeDialogController]
+ */
+@VolumeDialog
+class VolumeDialogStateInteractor
+@Inject
+constructor(
+ volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
+ private val volumeDialogController: VolumeDialogController,
+ @VolumeDialog private val coroutineScope: CoroutineScope,
+) {
+
+ val volumeDialogState: Flow<VolumeDialogStateModel> =
+ volumeDialogCallbacksInteractor.event
+ .onStart { volumeDialogController.getState() }
+ .filterIsInstance(VolumeDialogEventModel.StateChanged::class)
+ .map { it.state }
+ .stateIn(scope = coroutineScope, started = SharingStarted.Eagerly, initialValue = null)
+ .filterNotNull()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
new file mode 100644
index 0000000..ca0310e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.volume.dialog.domain.model
+
+import android.media.AudioManager
+
+/**
+ * Models VolumeDialogController callback events.
+ *
+ * @see VolumeDialogController.Callbacks
+ */
+sealed interface VolumeDialogEventModel {
+
+ data class ShowRequested(
+ val reason: Int,
+ val keyguardLocked: Boolean,
+ val lockTaskModeState: Int,
+ ) : VolumeDialogEventModel
+
+ data class DismissRequested(val reason: Int) : VolumeDialogEventModel
+
+ data class StateChanged(val state: VolumeDialogStateModel) : VolumeDialogEventModel
+
+ data class LayoutDirectionChanged(val layoutDirection: Int) : VolumeDialogEventModel
+
+ data object ShowVibrateHint : VolumeDialogEventModel
+
+ data object ShowSilentHint : VolumeDialogEventModel
+
+ data object ScreenOff : VolumeDialogEventModel
+
+ data class ShowSafetyWarning(val flags: Int) : VolumeDialogEventModel
+
+ data class AccessibilityModeChanged(val showA11yStream: Boolean) : VolumeDialogEventModel
+
+ data class ShowCsdWarning(@AudioManager.CsdWarning val csdWarning: Int, val durationMs: Int) :
+ VolumeDialogEventModel
+
+ data object VolumeChangedFromKey : VolumeDialogEventModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
new file mode 100644
index 0000000..f1443e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.volume.dialog.domain.model
+
+import android.content.ComponentName
+import android.util.SparseArray
+import androidx.core.util.keyIterator
+import com.android.systemui.plugins.VolumeDialogController
+
+/** Models a state of the Volume Dialog. */
+data class VolumeDialogStateModel(
+ val states: Map<Int, VolumeDialogStreamStateModel>,
+ val ringerModeInternal: Int = 0,
+ val ringerModeExternal: Int = 0,
+ val zenMode: Int = 0,
+ val effectsSuppressor: ComponentName? = null,
+ val effectsSuppressorName: String? = null,
+ val activeStream: Int = NO_ACTIVE_STREAM,
+ val disallowAlarms: Boolean = false,
+ val disallowMedia: Boolean = false,
+ val disallowSystem: Boolean = false,
+ val disallowRinger: Boolean = false,
+) {
+
+ constructor(
+ legacyState: VolumeDialogController.State
+ ) : this(
+ states = legacyState.states.mapToMap { VolumeDialogStreamStateModel(it) },
+ ringerModeInternal = legacyState.ringerModeInternal,
+ ringerModeExternal = legacyState.ringerModeExternal,
+ zenMode = legacyState.zenMode,
+ effectsSuppressor = legacyState.effectsSuppressor,
+ effectsSuppressorName = legacyState.effectsSuppressorName,
+ activeStream = legacyState.activeStream,
+ disallowAlarms = legacyState.disallowAlarms,
+ disallowMedia = legacyState.disallowMedia,
+ disallowSystem = legacyState.disallowSystem,
+ disallowRinger = legacyState.disallowRinger,
+ )
+
+ companion object {
+ const val NO_ACTIVE_STREAM: Int = -1
+ }
+}
+
+private fun <INPUT, OUTPUT> SparseArray<INPUT>.mapToMap(map: (INPUT) -> OUTPUT): Map<Int, OUTPUT> {
+ val resultMap = mutableMapOf<Int, OUTPUT>()
+ for (key in keyIterator()) {
+ val mappedValue: OUTPUT = map(get(key)!!)
+ resultMap[key] = mappedValue
+ }
+ return resultMap
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
new file mode 100644
index 0000000..a9d367d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.domain.model
+
+import android.annotation.IntegerRes
+import com.android.systemui.plugins.VolumeDialogController
+
+/** Models a state of an audio stream of the Volume Dialog. */
+data class VolumeDialogStreamStateModel(
+ val isDynamic: Boolean = false,
+ val level: Int = 0,
+ val levelMin: Int = 0,
+ val levelMax: Int = 0,
+ val muted: Boolean = false,
+ val muteSupported: Boolean = false,
+ @IntegerRes val name: Int = 0,
+ val remoteLabel: String? = null,
+ val routedToBluetooth: Boolean = false,
+) {
+ constructor(
+ legacyState: VolumeDialogController.StreamState
+ ) : this(
+ isDynamic = legacyState.dynamic,
+ level = legacyState.level,
+ levelMin = legacyState.levelMin,
+ levelMax = legacyState.levelMax,
+ muted = legacyState.muted,
+ muteSupported = legacyState.muteSupported,
+ name = legacyState.name,
+ remoteLabel = legacyState.remoteLabel,
+ routedToBluetooth = legacyState.routedToBluetooth,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
new file mode 100644
index 0000000..700225d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.volume.dialog.ui.binder
+
+import android.view.View
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.awaitCancellation
+
+class VolumeDialogViewBinder
+@Inject
+constructor(private val volumeDialogViewModelFactory: VolumeDialogViewModel.Factory) {
+
+ suspend fun bind(view: View) {
+ view.repeatWhenAttached {
+ view.viewModel(
+ traceName = "VolumeDialogViewBinder",
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { volumeDialogViewModelFactory.create() },
+ ) { viewModel ->
+ view.setSnapshotBinding {}
+
+ awaitCancellation()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
new file mode 100644
index 0000000..f9e91ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.dialog.ui.viewmodel
+
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
+
+ override suspend fun onActivated(): Nothing {
+ TODO("Not yet implemented")
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): VolumeDialogViewModel
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index e609d5f..34ebba8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -127,6 +127,7 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated;
import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
+import com.android.keyguard.logging.SimLogger;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
@@ -267,6 +268,8 @@
@Mock
private KeyguardUpdateMonitorLogger mKeyguardUpdateMonitorLogger;
@Mock
+ private SimLogger mSimLogger;
+ @Mock
private SessionTracker mSessionTracker;
@Mock
private UiEventLogger mUiEventLogger;
@@ -2234,6 +2237,36 @@
}
@Test
+ public void testOnSimStateChanged_LockedToNotReadyToLocked() {
+ int validSubId = 10;
+ int slotId = 0;
+
+ KeyguardUpdateMonitorCallback keyguardUpdateMonitorCallback = spy(
+ KeyguardUpdateMonitorCallback.class);
+ mKeyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback);
+ // Initially locked
+ mKeyguardUpdateMonitor.handleSimStateChange(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+ verify(keyguardUpdateMonitorCallback).onSimStateChanged(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+
+ reset(keyguardUpdateMonitorCallback);
+ // Not ready, with invalid sub id
+ mKeyguardUpdateMonitor.handleSimStateChange(SubscriptionManager.INVALID_SUBSCRIPTION_ID,
+ slotId, TelephonyManager.SIM_STATE_NOT_READY);
+ verify(keyguardUpdateMonitorCallback).onSimStateChanged(
+ SubscriptionManager.INVALID_SUBSCRIPTION_ID, slotId,
+ TelephonyManager.SIM_STATE_NOT_READY);
+
+ reset(keyguardUpdateMonitorCallback);
+ // Back to PIN required, which notifies listeners
+ mKeyguardUpdateMonitor.handleSimStateChange(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+ verify(keyguardUpdateMonitorCallback).onSimStateChanged(validSubId, slotId,
+ TelephonyManager.SIM_STATE_PIN_REQUIRED);
+ }
+
+ @Test
public void onAuthEnrollmentChangesCallbacksAreNotified() {
KeyguardUpdateMonitorCallback callback = mock(KeyguardUpdateMonitorCallback.class);
ArgumentCaptor<AuthController.Callback> authCallback = ArgumentCaptor.forClass(
@@ -2487,7 +2520,7 @@
mStatusBarStateController, mLockPatternUtils,
mAuthController, mTelephonyListenerManager,
mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
- mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
+ mKeyguardUpdateMonitorLogger, mSimLogger, mUiEventLogger, () -> mSessionTracker,
mTrustManager, mSubscriptionManager, mUserManager,
mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
mPackageManager, mFingerprintManager, mBiometricManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index 5fc1971..8075d11 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -104,6 +104,8 @@
@Mock
private Animator mAnimator;
+ @Mock
+ private Animator mEndAnimator;
private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor =
ArgumentCaptor.forClass(Animator.AnimatorListener.class);
@@ -123,7 +125,7 @@
MockitoAnnotations.initMocks(this);
when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator);
- when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator);
+ when(mClipboardOverlayView.getExitAnimation()).thenReturn(mEndAnimator);
when(mClipboardOverlayView.getFadeOutAnimation()).thenReturn(mAnimator);
when(mClipboardOverlayWindow.getWindowInsets()).thenReturn(
getImeInsets(new Rect(0, 0, 0, 0)));
@@ -318,11 +320,11 @@
mOverlayController.setClipData(mSampleClipData, "");
mCallbacks.onShareButtonTapped();
- verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
- mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+ verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "");
- verify(mClipboardOverlayView, times(1)).getFadeOutAnimation();
+ verify(mClipboardOverlayView, times(1)).getExitAnimation();
}
@Test
@@ -343,8 +345,8 @@
initController();
mCallbacks.onDismissButtonTapped();
- verify(mAnimator).addListener(mAnimatorListenerCaptor.capture());
- mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator);
+ verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
// package name is null since we haven't actually set a source for this test
verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, null);
@@ -403,14 +405,18 @@
mOverlayController.setClipData(mSampleClipData, "first.package");
mCallbacks.onShareButtonTapped();
+ verify(mEndAnimator).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
mOverlayController.setClipData(mSampleClipData, "second.package");
mCallbacks.onShareButtonTapped();
+ verify(mEndAnimator, times(2)).addListener(mAnimatorListenerCaptor.capture());
+ mAnimatorListenerCaptor.getValue().onAnimationEnd(mEndAnimator);
- verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
- verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "first.package");
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package");
verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "second.package");
+ verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package");
verifyNoMoreInteractions(mUiEventLogger);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
index ad25502..7d5a334 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalDatabaseMigrationsTest.kt
@@ -148,6 +148,31 @@
)
}
+ @Test
+ fun migrate3To4_addSpanYColumn_defaultValuePopulated() {
+ val databaseV3 = migrationTestHelper.createDatabase(DATABASE_NAME, version = 3)
+
+ val fakeWidgetsV3 =
+ listOf(
+ FakeCommunalWidgetItemV3(1, "test_widget_1", 11, 0),
+ FakeCommunalWidgetItemV3(2, "test_widget_2", 12, 10),
+ FakeCommunalWidgetItemV3(3, "test_widget_3", 13, 0),
+ )
+ databaseV3.insertWidgetsV3(fakeWidgetsV3)
+
+ databaseV3.verifyWidgetsV3(fakeWidgetsV3)
+
+ val databaseV4 =
+ migrationTestHelper.runMigrationsAndValidate(
+ name = DATABASE_NAME,
+ version = 4,
+ validateDroppedTables = false,
+ CommunalDatabase.MIGRATION_3_4,
+ )
+
+ databaseV4.verifyWidgetsV4(fakeWidgetsV3.map { it.getV4() })
+ }
+
private fun SupportSQLiteDatabase.insertWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
widgets.forEach { widget ->
execSQL(
@@ -157,6 +182,22 @@
}
}
+ private fun SupportSQLiteDatabase.insertWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) {
+ widgets.forEach { widget ->
+ execSQL(
+ "INSERT INTO communal_widget_table(" +
+ "widget_id, " +
+ "component_name, " +
+ "item_id, " +
+ "user_serial_number) " +
+ "VALUES(${widget.widgetId}, " +
+ "'${widget.componentName}', " +
+ "${widget.itemId}, " +
+ "${widget.userSerialNumber})"
+ )
+ }
+ }
+
private fun SupportSQLiteDatabase.verifyWidgetsV1(widgets: List<FakeCommunalWidgetItemV1>) {
val cursor = query("SELECT * FROM communal_widget_table")
assertThat(cursor.moveToFirst()).isTrue()
@@ -193,6 +234,42 @@
assertThat(cursor.isAfterLast).isTrue()
}
+ private fun SupportSQLiteDatabase.verifyWidgetsV3(widgets: List<FakeCommunalWidgetItemV3>) {
+ val cursor = query("SELECT * FROM communal_widget_table")
+ assertThat(cursor.moveToFirst()).isTrue()
+
+ widgets.forEach { widget ->
+ assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId)
+ assertThat(cursor.getString(cursor.getColumnIndex("component_name")))
+ .isEqualTo(widget.componentName)
+ assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId)
+ assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number")))
+ .isEqualTo(widget.userSerialNumber)
+
+ cursor.moveToNext()
+ }
+ assertThat(cursor.isAfterLast).isTrue()
+ }
+
+ private fun SupportSQLiteDatabase.verifyWidgetsV4(widgets: List<FakeCommunalWidgetItemV4>) {
+ val cursor = query("SELECT * FROM communal_widget_table")
+ assertThat(cursor.moveToFirst()).isTrue()
+
+ widgets.forEach { widget ->
+ assertThat(cursor.getInt(cursor.getColumnIndex("widget_id"))).isEqualTo(widget.widgetId)
+ assertThat(cursor.getString(cursor.getColumnIndex("component_name")))
+ .isEqualTo(widget.componentName)
+ assertThat(cursor.getInt(cursor.getColumnIndex("item_id"))).isEqualTo(widget.itemId)
+ assertThat(cursor.getInt(cursor.getColumnIndex("user_serial_number")))
+ .isEqualTo(widget.userSerialNumber)
+ assertThat(cursor.getInt(cursor.getColumnIndex("span_y"))).isEqualTo(widget.spanY)
+
+ cursor.moveToNext()
+ }
+
+ assertThat(cursor.isAfterLast).isTrue()
+ }
+
private fun SupportSQLiteDatabase.insertRanks(ranks: List<FakeCommunalItemRank>) {
ranks.forEach { rank ->
execSQL("INSERT INTO communal_item_rank_table(rank) VALUES(${rank.rank})")
@@ -238,10 +315,27 @@
val userSerialNumber: Int,
)
- private data class FakeCommunalItemRank(
- val rank: Int,
+ private fun FakeCommunalWidgetItemV3.getV4(): FakeCommunalWidgetItemV4 {
+ return FakeCommunalWidgetItemV4(widgetId, componentName, itemId, userSerialNumber, 3)
+ }
+
+ private data class FakeCommunalWidgetItemV3(
+ val widgetId: Int,
+ val componentName: String,
+ val itemId: Int,
+ val userSerialNumber: Int,
)
+ private data class FakeCommunalWidgetItemV4(
+ val widgetId: Int,
+ val componentName: String,
+ val itemId: Int,
+ val userSerialNumber: Int,
+ val spanY: Int,
+ )
+
+ private data class FakeCommunalItemRank(val rank: Int)
+
companion object {
private const val DATABASE_NAME = "communal_db"
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index 2f8f45c..0b9c06f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -901,7 +901,7 @@
// 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId()
verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
- assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+ assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size()).isEqualTo(2);
// Adds non DDS subId again
doReturn(SUB_ID2).when(info).getSubscriptionId();
@@ -912,7 +912,7 @@
// Does not add due to cached subInfo in mSubIdTelephonyCallbackMap.
verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
- assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+ assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size()).isEqualTo(2);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
index 148a2e5..52266ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
@@ -29,7 +29,6 @@
import java.util.UUID
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.kotlin.any
@@ -47,16 +46,11 @@
private val uiEventLogger = mock<UiEventLogger>()
private val actionsCallback = mock<ScreenshotActionsController.ActionsCallback>()
- private val request = ScreenshotData.forTesting()
+ private val request = ScreenshotData.forTesting(userHandle = UserHandle.OWNER)
private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0)
private lateinit var actionsProvider: ScreenshotActionsProvider
- @Before
- fun setUp() {
- request.userHandle = UserHandle.OWNER
- }
-
@Test
fun previewActionAccessed_beforeScreenshotCompleted_doesNothing() {
actionsProvider = createActionsProvider()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
index 15da77d..4000d6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -47,7 +47,7 @@
lateinit var screenshotView: ViewGroup
val userHandle = UserHandle.of(5)
- val screenshotData = ScreenshotData.forTesting()
+ val screenshotData = ScreenshotData.forTesting(userHandle = userHandle)
val appName = "app name"
lateinit var workProfileData: WorkProfileMessageController.WorkProfileFirstRunData
@@ -61,7 +61,7 @@
workProfileMessageController,
profileMessageController,
screenshotDetectionController,
- TestScope(UnconfinedTestDispatcher())
+ TestScope(UnconfinedTestDispatcher()),
)
screenshotView = ConstraintLayout(mContext)
workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
@@ -83,8 +83,6 @@
container.addView(detectionNoticeView)
messageContainer.setView(screenshotView)
-
- screenshotData.userHandle = userHandle
}
@Test
@@ -92,7 +90,7 @@
val profileData =
ProfileMessageController.ProfileFirstRunData(
LabeledIcon(appName, icon),
- ProfileMessageController.FirstRunProfile.PRIVATE
+ ProfileMessageController.FirstRunProfile.PRIVATE,
)
whenever(profileMessageController.onScreenshotTaken(eq(userHandle))).thenReturn(profileData)
messageContainer.onScreenshotTaken(screenshotData)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
index 1538c72..c5070286 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
@@ -61,8 +61,8 @@
@Test
fun testMaybeNotifyOfScreenshot_ignoresOverview() {
- val data = ScreenshotData.forTesting()
- data.source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW
+ val data =
+ ScreenshotData.forTesting(source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW)
val list = controller.maybeNotifyOfScreenshot(data)
@@ -72,8 +72,8 @@
@Test
fun testMaybeNotifyOfScreenshot_emptySet() {
- val data = ScreenshotData.forTesting()
- data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+ val data =
+ ScreenshotData.forTesting(source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD)
whenever(windowManager.notifyScreenshotListeners(eq(Display.DEFAULT_DISPLAY)))
.thenReturn(listOf())
@@ -85,8 +85,8 @@
@Test
fun testMaybeNotifyOfScreenshot_oneApp() {
- val data = ScreenshotData.forTesting()
- data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+ val data =
+ ScreenshotData.forTesting(source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD)
val component = ComponentName("package1", "class1")
val appName = "app name"
@@ -95,7 +95,7 @@
whenever(
packageManager.getActivityInfo(
eq(component),
- any(PackageManager.ComponentInfoFlags::class.java)
+ any(PackageManager.ComponentInfoFlags::class.java),
)
)
.thenReturn(activityInfo)
@@ -112,8 +112,8 @@
@Test
fun testMaybeNotifyOfScreenshot_multipleApps() {
- val data = ScreenshotData.forTesting()
- data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+ val data =
+ ScreenshotData.forTesting(source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD)
val component1 = ComponentName("package1", "class1")
val component2 = ComponentName("package2", "class2")
@@ -129,21 +129,21 @@
whenever(
packageManager.getActivityInfo(
eq(component1),
- any(PackageManager.ComponentInfoFlags::class.java)
+ any(PackageManager.ComponentInfoFlags::class.java),
)
)
.thenReturn(activityInfo1)
whenever(
packageManager.getActivityInfo(
eq(component2),
- any(PackageManager.ComponentInfoFlags::class.java)
+ any(PackageManager.ComponentInfoFlags::class.java),
)
)
.thenReturn(activityInfo2)
whenever(
packageManager.getActivityInfo(
eq(component3),
- any(PackageManager.ComponentInfoFlags::class.java)
+ any(PackageManager.ComponentInfoFlags::class.java),
)
)
.thenReturn(activityInfo3)
@@ -165,11 +165,13 @@
private fun includesFlagBits(@PackageManager.ComponentInfoFlagsBits mask: Int) =
ComponentInfoFlagMatcher(mask, mask)
+
private fun excludesFlagBits(@PackageManager.ComponentInfoFlagsBits mask: Int) =
ComponentInfoFlagMatcher(mask, 0)
private class ComponentInfoFlagMatcher(
- @PackageManager.ComponentInfoFlagsBits val mask: Int, val value: Int
+ @PackageManager.ComponentInfoFlagsBits val mask: Int,
+ val value: Int,
) : ArgumentMatcher<PackageManager.ComponentInfoFlags> {
override fun matches(flags: PackageManager.ComponentInfoFlags?): Boolean {
return flags != null && (mask.toLong() and flags.value) == value.toLong()
@@ -182,26 +184,28 @@
@Test
fun testMaybeNotifyOfScreenshot_disabledApp() {
- val data = ScreenshotData.forTesting()
- data.source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
+ val data =
+ ScreenshotData.forTesting(source = WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD)
val component = ComponentName("package1", "class1")
val appName = "app name"
val activityInfo = mock(ActivityInfo::class.java)
whenever(
- packageManager.getActivityInfo(
- eq(component),
- argThat(includesFlagBits(MATCH_DISABLED_COMPONENTS or MATCH_ANY_USER))
+ packageManager.getActivityInfo(
+ eq(component),
+ argThat(includesFlagBits(MATCH_DISABLED_COMPONENTS or MATCH_ANY_USER)),
+ )
)
- ).thenReturn(activityInfo)
+ .thenReturn(activityInfo)
whenever(
- packageManager.getActivityInfo(
- eq(component),
- argThat(excludesFlagBits(MATCH_DISABLED_COMPONENTS))
+ packageManager.getActivityInfo(
+ eq(component),
+ argThat(excludesFlagBits(MATCH_DISABLED_COMPONENTS)),
+ )
)
- ).thenThrow(PackageManager.NameNotFoundException::class.java)
+ .thenThrow(PackageManager.NameNotFoundException::class.java)
whenever(windowManager.notifyScreenshotListeners(eq(Display.DEFAULT_DISPLAY)))
.thenReturn(listOf(component))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index a295981..0bea560 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -3,6 +3,8 @@
import android.content.ComponentName
import android.graphics.Bitmap
import android.net.Uri
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.Display.TYPE_INTERNAL
@@ -15,6 +17,7 @@
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.util.ScreenshotRequest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.display.data.repository.display
@@ -75,6 +78,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
testScope.runTest {
val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -106,6 +110,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
testScope.runTest {
val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -115,7 +120,7 @@
screenshotExecutor.executeScreenshots(
createScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE),
onSaved,
- callback
+ callback,
)
verify(controllerFactory).create(eq(internalDisplay))
@@ -137,6 +142,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_onlyVirtualDisplays_noInteractionsWithControllers() =
testScope.runTest {
setDisplays(display(TYPE_VIRTUAL, id = 0), display(TYPE_VIRTUAL, id = 1))
@@ -149,6 +155,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_allowedTypes_allCaptured() =
testScope.runTest {
whenever(controllerFactory.create(any())).thenReturn(controller)
@@ -157,7 +164,7 @@
display(TYPE_INTERNAL, id = 0),
display(TYPE_EXTERNAL, id = 1),
display(TYPE_OVERLAY, id = 2),
- display(TYPE_WIFI, id = 3)
+ display(TYPE_WIFI, id = 3),
)
val onSaved = { _: Uri? -> }
screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -168,6 +175,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_reportsOnFinishedOnlyWhenBothFinished() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -193,6 +201,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_oneFinishesOtherFails_reportFailsOnlyAtTheEnd() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -220,6 +229,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_allDisplaysFail_reportsFail() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -247,6 +257,58 @@
}
@Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_fromOverview_honorsDisplay() =
+ testScope.runTest {
+ val displayId = 1
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = displayId))
+ val onSaved = { _: Uri? -> }
+ screenshotExecutor.executeScreenshots(
+ createScreenshotRequest(
+ displayId = displayId,
+ source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW,
+ ),
+ onSaved,
+ callback,
+ )
+
+ val dataCaptor = ArgumentCaptor<ScreenshotData>()
+
+ verify(controller).handleScreenshot(dataCaptor.capture(), any(), any())
+
+ assertThat(dataCaptor.value.displayId).isEqualTo(displayId)
+
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_fromOverviewInvalidDisplay_usesDefault() =
+ testScope.runTest {
+ setDisplays(
+ display(TYPE_INTERNAL, id = Display.DEFAULT_DISPLAY),
+ display(TYPE_EXTERNAL, id = 1),
+ )
+ val onSaved = { _: Uri? -> }
+ screenshotExecutor.executeScreenshots(
+ createScreenshotRequest(
+ displayId = 5,
+ source = WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW,
+ ),
+ onSaved,
+ callback,
+ )
+
+ val dataCaptor = ArgumentCaptor<ScreenshotData>()
+
+ verify(controller).handleScreenshot(dataCaptor.capture(), any(), any())
+
+ assertThat(dataCaptor.value.displayId).isEqualTo(Display.DEFAULT_DISPLAY)
+
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
fun onDestroy_propagatedToControllers() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -319,6 +381,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_errorFromProcessor_logsScreenshotRequested() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -336,6 +399,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
fun executeScreenshots_errorFromProcessor_logsUiError() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -379,7 +443,8 @@
}
@Test
- fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_multidisplay_reportsRequested() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
@@ -399,7 +464,27 @@
}
@Test
- fun executeScreenshots_errorFromScreenshotController_reportsError() =
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
+ testScope.runTest {
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val onSaved = { _: Uri? -> }
+ whenever(controller.handleScreenshot(any(), any(), any()))
+ .thenThrow(IllegalStateException::class.java)
+
+ screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+ val screenshotRequested =
+ eventLogger.logs.filter {
+ it.eventId == ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER.id
+ }
+ assertThat(screenshotRequested).hasSize(1)
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_multidisplay_reportsError() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
@@ -419,7 +504,27 @@
}
@Test
- fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_reportsError() =
+ testScope.runTest {
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val onSaved = { _: Uri? -> }
+ whenever(controller.handleScreenshot(any(), any(), any()))
+ .thenThrow(IllegalStateException::class.java)
+
+ screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+ val screenshotRequested =
+ eventLogger.logs.filter {
+ it.eventId == ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED.id
+ }
+ assertThat(screenshotRequested).hasSize(1)
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_multidisplay_showsErrorNotification() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
val onSaved = { _: Uri? -> }
@@ -436,6 +541,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_SCREENSHOT_MULTIDISPLAY_FOCUS_CHANGE)
+ fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
+ testScope.runTest {
+ setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
+ val onSaved = { _: Uri? -> }
+ whenever(controller.handleScreenshot(any(), any(), any()))
+ .thenThrow(IllegalStateException::class.java)
+
+ screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
+
+ verify(notificationsController0).notifyScreenshotError(any())
+ screenshotExecutor.onDestroy()
+ }
+
+ @Test
fun executeScreenshots_finisherCalledWithNullUri_succeeds() =
testScope.runTest {
setDisplays(display(TYPE_INTERNAL, id = 0))
@@ -459,9 +579,14 @@
runCurrent()
}
- private fun createScreenshotRequest(type: Int = WindowManager.TAKE_SCREENSHOT_FULLSCREEN) =
- ScreenshotRequest.Builder(type, WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER)
+ private fun createScreenshotRequest(
+ type: Int = WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ source: Int = WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER,
+ displayId: Int = Display.DEFAULT_DISPLAY,
+ ) =
+ ScreenshotRequest.Builder(type, source)
.setTopComponent(topComponent)
+ .setDisplayId(displayId)
.also {
if (type == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
it.setBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
new file mode 100644
index 0000000..040a9e9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.shared.clocks.FontTextStyle
+import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SimpleDigitalClockTextViewTest : SysuiTestCase() {
+ private val messageBuffer = LogUtil.DEBUG_MESSAGE_BUFFER
+ private lateinit var underTest: SimpleDigitalClockTextView
+ private val defaultLargeClockTextSize = 500F
+ private val smallerTextSize = 300F
+ private val largerTextSize = 800F
+ private val firstMeasureTextSize = 100F
+
+ @Before
+ fun setup() {
+ underTest = SimpleDigitalClockTextView(context, messageBuffer)
+ underTest.textStyle = FontTextStyle()
+ underTest.aodStyle = FontTextStyle()
+ underTest.text = "0"
+ underTest.applyTextSize(defaultLargeClockTextSize)
+ }
+
+ @Test
+ fun applySmallerConstrainedTextSize_applyConstrainedTextSize() {
+ underTest.applyTextSize(smallerTextSize, constrainedByHeight = true)
+ assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor)
+ }
+
+ @Test
+ fun applyLargerConstrainedTextSize_applyUnconstrainedTextSize() {
+ underTest.applyTextSize(largerTextSize, constrainedByHeight = true)
+ assertEquals(defaultLargeClockTextSize, underTest.textSize)
+ }
+
+ @Test
+ fun applyFirstMeasureConstrainedTextSize_getConstrainedTextSize() {
+ underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+ underTest.applyTextSize(smallerTextSize, constrainedByHeight = true)
+ assertEquals(smallerTextSize, underTest.textSize * underTest.fontSizeAdjustFactor)
+ }
+
+ @Test
+ fun applySmallFirstMeasureConstrainedSizeAndLargerConstrainedTextSize_applyDefaultSize() {
+ underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+ underTest.applyTextSize(largerTextSize, constrainedByHeight = true)
+ assertEquals(defaultLargeClockTextSize, underTest.textSize)
+ }
+
+ @Test
+ fun applyFirstMeasureConstrainedTextSize_applyUnconstrainedTextSize() {
+ underTest.applyTextSize(firstMeasureTextSize, constrainedByHeight = true)
+ underTest.applyTextSize(defaultLargeClockTextSize)
+ assertEquals(defaultLargeClockTextSize, underTest.textSize)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 7a8533e..fe287ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -23,6 +23,7 @@
import android.app.smartspace.SmartspaceTarget
import android.content.ComponentName
import android.content.ContentResolver
+import android.content.Context
import android.content.pm.UserInfo
import android.database.ContentObserver
import android.graphics.drawable.Drawable
@@ -207,6 +208,9 @@
private val userHandleManaged: UserHandle = UserHandle(2)
private val userHandleSecondary: UserHandle = UserHandle(3)
+ @Mock private lateinit var userContextPrimary: Context
+ @Mock private lateinit var userContextSecondary: Context
+
private val userList = listOf(
mockUserInfo(userHandlePrimary, isManagedProfile = false),
mockUserInfo(userHandleManaged, isManagedProfile = true),
@@ -234,7 +238,11 @@
`when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true)
`when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true)
- setActiveUser(userHandlePrimary)
+ `when`(userContextPrimary.getSystemService(SmartspaceManager::class.java)).thenReturn(
+ smartspaceManager
+ )
+
+ setActiveUser(userHandlePrimary, userContextPrimary)
setAllowPrivateNotifications(userHandlePrimary, true)
setAllowPrivateNotifications(userHandleManaged, true)
setAllowPrivateNotifications(userHandleSecondary, true)
@@ -252,7 +260,6 @@
controller = LockscreenSmartspaceController(
context,
featureFlags,
- smartspaceManager,
activityStarter,
falsingManager,
clock,
@@ -709,7 +716,8 @@
connectSession()
// WHEN the secondary user becomes the active user
- setActiveUser(userHandleSecondary)
+ // Note: it doesn't switch to the SmartspaceManager for userContextSecondary
+ setActiveUser(userHandleSecondary, userContextSecondary)
userListener.onUserChanged(userHandleSecondary.identifier, context)
// WHEN we receive a new list of targets
@@ -912,9 +920,10 @@
clearInvocations(smartspaceView)
}
- private fun setActiveUser(userHandle: UserHandle) {
+ private fun setActiveUser(userHandle: UserHandle, userContext: Context) {
`when`(userTracker.userId).thenReturn(userHandle.identifier)
`when`(userTracker.userHandle).thenReturn(userHandle)
+ `when`(userTracker.userContext).thenReturn(userContext)
}
private fun mockUserInfo(userHandle: UserHandle, isManagedProfile: Boolean): UserInfo {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
index c523819..81c40dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackTest.java
@@ -36,7 +36,9 @@
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.ActionClickLogger;
@@ -50,8 +52,11 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.time.FakeSystemClock;
+import dagger.Lazy;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -71,8 +76,14 @@
@Mock private SysuiStatusBarStateController mStatusBarStateController;
@Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@Mock private ActivityStarter mActivityStarter;
+ @Mock private Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
+ @Mock private Lazy<SceneInteractor> mSceneInteractorLazy;
+ @Mock private JavaAdapter mJavaAdapter;
private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+ @Mock private DeviceUnlockedInteractor mDeviceUnlockedInteractor;
+ @Mock private SceneInteractor mSceneInteractor;
+
private int mCurrentUserId = 0;
private StatusBarRemoteInputCallback mRemoteInputCallback;
@@ -90,7 +101,8 @@
mKeyguardStateController, mStatusBarStateController, mStatusBarKeyguardViewManager,
mActivityStarter, mShadeController,
new CommandQueue(mContext, new FakeDisplayTracker(mContext)),
- mock(ActionClickLogger.class), mFakeExecutor));
+ mock(ActionClickLogger.class), mFakeExecutor, mDeviceUnlockedInteractorLazy,
+ mSceneInteractorLazy, mJavaAdapter));
mRemoteInputCallback.mChallengeReceiver = mRemoteInputCallback.new ChallengeReceiver();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index f62beeb..beba0f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -133,10 +133,6 @@
when(mRingerModeInternalLiveData.getValue()).thenReturn(-1);
when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
when(mUserTracker.getUserContext()).thenReturn(mContext);
- // Enable group volume adjustments
- mContext.getOrCreateTestableResources().addOverride(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions,
- true);
mCallback = mock(VolumeDialogControllerImpl.C.class);
mThreadFactory.setLooper(TestableLooper.get(this).getLooper());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 5d7e7c7..1302faa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -31,7 +31,7 @@
provider: ComponentName,
user: UserHandle,
rank: Int?,
- configurator: WidgetConfigurator?
+ configurator: WidgetConfigurator?,
) {
coroutineScope.launch {
val id = nextWidgetId++
@@ -93,6 +93,22 @@
_communalWidgets.value = fakeDatabase.values.toList()
}
+ override fun updateWidgetSpanY(widgetId: Int, spanY: Int) {
+ coroutineScope.launch {
+ fakeDatabase[widgetId]?.let { widget ->
+ when (widget) {
+ is CommunalWidgetContentModel.Available -> {
+ fakeDatabase[widgetId] = widget.copy(spanY = spanY)
+ }
+ is CommunalWidgetContentModel.Pending -> {
+ fakeDatabase[widgetId] = widget.copy(spanY = spanY)
+ }
+ }
+ _communalWidgets.value = fakeDatabase.values.toList()
+ }
+ }
+ }
+
override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {}
override fun abortRestoreWidgets() {}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
index b37cac1..ba31683 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeKeyboardRepository.kt
@@ -19,8 +19,10 @@
import com.android.systemui.keyboard.data.model.Keyboard
import com.android.systemui.keyboard.shared.model.BacklightModel
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.filterNotNull
class FakeKeyboardRepository : KeyboardRepository {
@@ -32,8 +34,14 @@
// filtering to make sure backlight doesn't have default initial value
override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull()
- private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null)
- override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull()
+ // implemented as channel because original implementation is modeling events: it doesn't hold
+ // state so it won't always emit once connected. And it's bad if some tests depend on that
+ // incorrect behaviour.
+ private val _newlyConnectedKeyboard: Channel<Keyboard> = Channel()
+ override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.consumeAsFlow()
+
+ private val _connectedKeyboards: MutableStateFlow<Set<Keyboard>> = MutableStateFlow(setOf())
+ override val connectedKeyboards: Flow<Set<Keyboard>> = _connectedKeyboards
fun setBacklight(state: BacklightModel) {
_backlightState.value = state
@@ -43,7 +51,14 @@
_isAnyKeyboardConnected.value = connected
}
+ fun setConnectedKeyboards(keyboards: Set<Keyboard>) {
+ _connectedKeyboards.value = keyboards
+ _isAnyKeyboardConnected.value = keyboards.isNotEmpty()
+ }
+
fun setNewlyConnectedKeyboard(keyboard: Keyboard) {
- _newlyConnectedKeyboard.value = keyboard
+ _newlyConnectedKeyboard.trySend(keyboard)
+ _connectedKeyboards.value += keyboard
+ _isAnyKeyboardConnected.value = true
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
index f26bb83..805a710 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt
@@ -35,6 +35,8 @@
private val _revealAmount: MutableStateFlow<Float> = MutableStateFlow(0.0f)
override val revealAmount: Flow<Float> = _revealAmount
+ val revealAnimatorRequests: MutableList<RevealAnimatorRequest> = arrayListOf()
+
override val isAnimating: Boolean
get() = false
@@ -44,5 +46,12 @@
} else {
_revealAmount.value = 0.0f
}
+
+ revealAnimatorRequests.add(RevealAnimatorRequest(reveal, duration))
}
+
+ data class RevealAnimatorRequest(
+ val reveal: Boolean,
+ val duration: Long
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRule.kt
new file mode 100644
index 0000000..e4c793d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRule.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.util
+
+import android.testing.UiThreadTest
+import org.junit.Assert.fail
+import org.junit.rules.MethodRule
+import org.junit.runners.model.FrameworkMethod
+import org.junit.runners.model.Statement
+
+/**
+ * A Test rule which prevents us from using the UiThreadTest annotation. See
+ * go/android_junit4_uithreadtest (b/352170965)
+ */
+public class NoUiThreadTestRule : MethodRule {
+ override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement? {
+ if (hasUiThreadAnnotation(method, target)) {
+ fail("UiThreadTest doesn't actually run on the UiThread")
+ }
+ return base
+ }
+
+ private fun hasUiThreadAnnotation(method: FrameworkMethod, target: Any): Boolean {
+ if (method.getAnnotation(UiThreadTest::class.java) != null) {
+ return true
+ } else {
+ return target.javaClass.getAnnotation(UiThreadTest::class.java) != null
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRuleTest.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRuleTest.kt
new file mode 100644
index 0000000..70dd103
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/NoUiThreadTestRuleTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.util
+
+import android.testing.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.lang.AssertionError
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.model.FrameworkMethod
+import org.junit.runners.model.Statement
+
+/**
+ * Test that NoUiThreadTestRule asserts when it finds a framework method with a UiThreadTest
+ * annotation.
+ */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+public class NoUiThreadTestRuleTest : SysuiTestCase() {
+
+ class TestStatement : Statement() {
+ override fun evaluate() {}
+ }
+
+ inner class TestInner {
+ @Test @UiThreadTest fun simpleUiTest() {}
+
+ @Test fun simpleTest() {}
+ }
+
+ /**
+ * Test that NoUiThreadTestRule throws an asserts false if a test is annotated
+ * with @UiThreadTest
+ */
+ @Test(expected = AssertionError::class)
+ fun testNoUiThreadFail() {
+ val method = TestInner::class.java.getDeclaredMethod("simpleUiTest")
+ val frameworkMethod = FrameworkMethod(method)
+ val noUiThreadTestRule = NoUiThreadTestRule()
+ val testStatement = TestStatement()
+ // target needs to be non-null
+ val obj = Object()
+ noUiThreadTestRule.apply(testStatement, frameworkMethod, obj)
+ }
+
+ /**
+ * Test that NoUiThreadTestRule throws an asserts false if a test is annotated
+ * with @UiThreadTest
+ */
+ fun testNoUiThreadOK() {
+ val method = TestInner::class.java.getDeclaredMethod("simpleUiTest")
+ val frameworkMethod = FrameworkMethod(method)
+ val noUiThreadTestRule = NoUiThreadTestRule()
+ val testStatement = TestStatement()
+
+ // because target needs to be non-null
+ val obj = Object()
+ val newStatement = noUiThreadTestRule.apply(testStatement, frameworkMethod, obj)
+ Assert.assertEquals(newStatement, testStatement)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThread.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThread.java
new file mode 100644
index 0000000..f81b7de
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThread.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util;
+
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * A class to launch runnables on the UI thread explicitly.
+ */
+public class UiThread {
+ private static final String TAG = "UiThread";
+
+ /**
+ * Run a runnable on the UI thread using instrumentation.runOnMainSync.
+ *
+ * @param runnable code to run on the UI thread.
+ * @throws Throwable if the code threw an exception, so it can be reported
+ * to the test.
+ */
+ public static void runOnUiThread(final Runnable runnable) throws Throwable {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ Log.w(
+ TAG,
+ "UiThread.runOnUiThread() should not be called from the "
+ + "main application thread");
+ runnable.run();
+ } else {
+ FutureTask<Void> task = new FutureTask<>(runnable, null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(task);
+ try {
+ task.get();
+ } catch (ExecutionException e) {
+ // Expose the original exception
+ throw e.getCause();
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThreadRunTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThreadRunTest.java
new file mode 100644
index 0000000..abf2e8d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/UiThreadRunTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util;
+
+import android.os.Looper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Test that UiThread.runOnUiThread() actually runs on the UI Thread.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UiThreadRunTest extends SysuiTestCase {
+
+ @Test
+ public void testUiThread() throws Throwable {
+ UiThread.runOnUiThread(() -> {
+ Assert.assertEquals(Looper.getMainLooper().getThread(), Thread.currentThread());
+ });
+ }
+}
diff --git a/packages/SystemUI/utils/kairos/Android.bp b/packages/SystemUI/utils/kairos/Android.bp
new file mode 100644
index 0000000..1442591
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+java_library {
+ name: "kairos",
+ host_supported: true,
+ kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalFrpApi"],
+ srcs: ["src/**/*.kt"],
+ static_libs: [
+ "kotlin-stdlib",
+ "kotlinx_coroutines",
+ ],
+}
+
+java_test {
+ name: "kairos-test",
+ optimize: {
+ enabled: false,
+ },
+ srcs: [
+ "test/**/*.kt",
+ ],
+ static_libs: [
+ "kairos",
+ "junit",
+ "kotlin-stdlib",
+ "kotlin-test",
+ "kotlinx_coroutines",
+ "kotlinx_coroutines_test",
+ ],
+}
diff --git a/packages/SystemUI/utils/kairos/OWNERS b/packages/SystemUI/utils/kairos/OWNERS
new file mode 100644
index 0000000..8876ad6
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/OWNERS
@@ -0,0 +1,3 @@
+steell@google.com
+nijamkin@google.com
+evanlaird@google.com
diff --git a/packages/SystemUI/utils/kairos/README.md b/packages/SystemUI/utils/kairos/README.md
new file mode 100644
index 0000000..85f622c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/README.md
@@ -0,0 +1,64 @@
+# Kairos
+
+A functional reactive programming (FRP) library for Kotlin.
+
+This library is **experimental** and should not be used for general production
+code. The APIs within are subject to change, and there may be bugs.
+
+## About FRP
+
+Functional reactive programming is a type of reactive programming system that
+follows a set of clear and composable rules, without sacrificing consistency.
+FRP exposes an API that should be familiar to those versed in Kotlin `Flow`.
+
+### Details for nerds
+
+`Kairos` implements an applicative / monadic flavor of FRP, using a push-pull
+methodology to allow for efficient updates.
+
+"Real" functional reactive programming should be specified with denotational
+semantics ([wikipedia](https://en.wikipedia.org/wiki/Denotational_semantics)):
+you can view the semantics for `Kairos` [here](docs/semantics.md).
+
+## Usage
+
+First, stand up a new `FrpNetwork`. All reactive events and state is kept
+consistent within a single network.
+
+``` kotlin
+val coroutineScope: CoroutineScope = ...
+val frpNetwork = coroutineScope.newFrpNetwork()
+```
+
+You can use the `FrpNetwork` to stand-up a network of reactive events and state.
+Events are modeled with `TFlow` (short for "transactional flow"), and state
+`TState` (short for "transactional state").
+
+``` kotlin
+suspend fun activate(network: FrpNetwork) {
+ network.activateSpec {
+ val input = network.mutableTFlow<Unit>()
+ // Launch a long-running side-effect that emits to the network
+ // every second.
+ launchEffect {
+ while (true) {
+ input.emit(Unit)
+ delay(1.seconds)
+ }
+ }
+ // Accumulate state
+ val count: TState<Int> = input.fold { _, i -> i + 1 }
+ // Observe events to perform side-effects in reaction to them
+ input.observe {
+ println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}")
+ }
+ }
+}
+```
+
+`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation
+will tear-down all effects and obervers running within the lambda.
+
+## Resources
+
+- [Cheatsheet for those coming from Kotlin Flow](docs/flow-to-kairos-cheatsheet.md)
diff --git a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md
new file mode 100644
index 0000000..9f7fd02
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md
@@ -0,0 +1,330 @@
+# From Flows to Kairos
+
+## Key differences
+
+* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction.
+
+* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState`
+
+ * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that
+ exists for the duration of the current Kairos transaction and shared with
+ `SharingStarted.WhileSubscribed()`
+
+ * `TState` is roughly equivalent to `StateFlow` shared with
+ `SharingStarted.Eagerly`, but the current value can only be queried within
+ a Kairos transaction, and the value is only updated at the end of the
+ transaction
+
+* Kairos further divides `Flow` APIs based on how they internally use state:
+
+ * **FrpTransactionScope:** APIs that internally query some state need to be
+ performed within an Kairos transaction
+
+ * this scope is available from the other scopes, and from most lambdas
+ passed to other Kairos APIs
+
+ * **FrpStateScope:** APIs that internally accumulate state in reaction to
+ events need to be performed within an FRP State scope (akin to a
+ `CoroutineScope`)
+
+ * this scope is a side-effect-free subset of FrpBuildScope, and so can be
+ used wherever you have an FrpBuildScope
+
+ * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`)
+ need to be performed within an FRP Build scope (akin to a `CoroutineScope`)
+
+ * this scope is available from `FrpNetwork.activateSpec { … }`
+
+ * All other APIs can be used anywhere
+
+## emptyFlow()
+
+Use `emptyTFlow`
+
+``` kotlin
+// this TFlow emits nothing
+val noEvents: TFlow<Int> = emptyTFlow
+```
+
+## map { … }
+
+Use `TFlow.map` / `TState.map`
+
+``` kotlin
+val anInt: TState<Int> = …
+val squared: TState<Int> = anInt.map { it * it }
+val messages: TFlow<String> = …
+val messageLengths: TFlow<Int> = messages.map { it.size }
+```
+
+## filter { … } / mapNotNull { … }
+
+### I have a TFlow
+
+Use `TFlow.filter` / `TFlow.mapNotNull`
+
+``` kotlin
+val messages: TFlow<String> = …
+val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() }
+```
+
+### I have a TState
+
+Convert the `TState` to `TFlow` using `TState.stateChanges`, then use
+`TFlow.filter` / `TFlow.mapNotNull`
+
+If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the
+result.
+
+``` kotlin
+tState.stateChanges.filter { … }.hold(initialValue)
+```
+
+Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
+track the lifetime of the state accumulation.
+
+## combine(...) { … }
+
+### I have TStates
+
+Use `combine(TStates)`
+
+``` kotlin
+val someInt: TState<Int> = …
+val someString: TState<String> = …
+val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }
+```
+
+### I have TFlows
+
+Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use
+`combine(TStates)`
+
+If you want the behavior of Flow.combine where nothing is emitted until each
+TFlow has emitted at least once, you can use filter:
+
+``` kotlin
+// null used as an example, can use a different sentinel if needed
+combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b ->
+ a?.let { b?.let { … } }
+ }
+ .filterNotNull()
+```
+
+Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
+track the lifetime of the state accumulation.
+
+#### Explanation
+
+`Flow.combine` always tracks the last-emitted value of each `Flow` it's
+combining. This is a form of state-accumulation; internally, it collects from
+each `Flow`, tracks the latest-emitted value, and when anything changes, it
+re-runs the lambda to combine the latest values.
+
+An effect of this is that `Flow.combine` doesn't emit until each combined `Flow`
+has emitted at least once. This often bites developers. As a workaround,
+developers generally append `.onStart { emit(initialValue) }` to the `Flows`
+that don't immediately emit.
+
+Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus
+ensuring that there is always a current value to be combined for each input.
+
+## collect { … }
+
+Use `observe { … }`
+
+``` kotlin
+val job: Job = tFlow.observe { println("observed: $it") }
+```
+
+Note that `observe` is only available within an `FrpBuildScope` in order to
+track the lifetime of the observer. `FrpBuildScope` can only come from a
+top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a
+`-Latest` operator.
+
+## sample(flow) { … }
+
+### I want to sample a TState
+
+Use `TState.sample()` to get the current value of a `TState`. This can be
+invoked anywhere you have access to an `FrpTransactionScope`.
+
+``` kotlin
+// the lambda passed to map receives an FrpTransactionScope, so it can invoke
+// sample
+tFlow.map { tState.sample() }
+```
+
+#### Explanation
+
+To keep all state-reads consistent, the current value of a TState can only be
+queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note
+that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`.
+
+### I want to sample a TFlow
+
+Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`.
+
+Note that `hold` is only available within an `FrpStateScope` in order to track
+the lifetime of the state accumulation.
+
+## stateIn(scope, sharingStarted, initialValue)
+
+Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted
+argument; all states are accumulated eagerly.
+
+``` kotlin
+val ints: TFlow<Int> = …
+val lastSeenInt: TState<Int> = ints.hold(initialValue = 0)
+```
+
+Note that `hold` is only available within an `FrpStateScope` in order to track
+the lifetime of the state accumulation (akin to the scope parameter of
+`Flow.stateIn`). `FrpStateScope` can only come from a top-level
+`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest`
+operator. Also note that `FrpBuildScope` extends `FrpStateScope`.
+
+## distinctUntilChanged()
+
+Use `distinctUntilChanged` like normal. This is only available for `TFlow`;
+`TStates` are already `distinctUntilChanged`.
+
+## merge(...)
+
+### I have TFlows
+
+Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple
+simultaneous emissions within the same transaction.
+
+#### Explanation
+
+Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This
+means that if we are merging two or more `TFlows` that are emitting at the same
+time (within the same transaction), the resulting merged `TFlow` must emit a
+single value. The lambda argument allows the developer to decide what to do in
+this case.
+
+### I have TStates
+
+If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to
+convert to a `TFlow`, and then `merge`.
+
+## conflatedCallbackFlow { … }
+
+Use `tFlow { … }`.
+
+As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can
+convert it to a TFlow via `Flow.toTFlow()`.
+
+Note that `tFlow` is only available within an `FrpBuildScope` in order to track
+the lifetime of the input registration.
+
+## first()
+
+### I have a TState
+
+Use `TState.sample`.
+
+### I have a TFlow
+
+Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of
+suspending it returns a `TFlow` that emits once.
+
+The naming is intentionally different because `first` implies that it is the
+first-ever value emitted from the `Flow` (which makes sense for cold `Flows`),
+whereas `nextOnly` indicates that only the next value relative to the current
+transaction (the one `nextOnly` is being invoked in) will be emitted.
+
+Note that `nextOnly` is only available within an `FrpStateScope` in order to
+track the lifetime of the state accumulation.
+
+## flatMapLatest { … }
+
+If you want to use -Latest to cancel old side-effects, similar to what the Flow
+-Latest operators offer for coroutines, see `mapLatest`.
+
+### I have a TState…
+
+#### …and want to switch TStates
+
+Use `TState.flatMap`
+
+``` kotlin
+val flattened = tState.flatMap { a -> getTState(a) }
+```
+
+#### …and want to switch TFlows
+
+Use `TState<TFlow<T>>.switch()`
+
+``` kotlin
+val tFlow = tState.map { a -> getTFlow(a) }.switch()
+```
+
+### I have a TFlow…
+
+#### …and want to switch TFlows
+
+Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to
+the latest `TFlow`.
+
+``` kotlin
+val tFlow = tFlowOfFlows.hold(emptyTFlow).switch()
+```
+
+#### …and want to switch TStates
+
+Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to
+the latest `TState`.
+
+``` kotlin
+val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it }
+```
+
+## mapLatest { … } / collectLatest { … }
+
+`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that
+automatically cancel old work when new values are emitted.
+
+``` kotlin
+val currentModel: TState<SomeModel> = …
+val mapped: TState<...> = currentModel.mapLatestBuild { model ->
+ effect { "new model in the house: $model" }
+ model.someState.observe { "someState: $it" }
+ val someData: TState<SomeInfo> =
+ getBroadcasts(model.uri)
+ .map { extractInfo(it) }
+ .hold(initialInfo)
+ …
+}
+```
+
+## flowOf(...)
+
+### I want a TState
+
+Use `tStateOf(initialValue)`.
+
+### I want a TFlow
+
+Use `now.map { initialValue }`
+
+Note that `now` is only available within an `FrpTransactionScope`.
+
+#### Explanation
+
+`TFlows` are not cold, and so there isn't a notion of "emit this value once
+there is a collector" like there is for `Flow`. The closest analog would be
+`TState`, since the initial value is retained indefinitely until there is an
+observer. However, it is often useful to immediately emit a value within the
+current transaction, usually when using a `flatMap` or `switch`. In these cases,
+using `now` explicitly models that the emission will occur within the current
+transaction.
+
+``` kotlin
+fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value }
+```
+
+## MutableStateFlow / MutableSharedFlow
+
+Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`.
diff --git a/packages/SystemUI/utils/kairos/docs/semantics.md b/packages/SystemUI/utils/kairos/docs/semantics.md
new file mode 100644
index 0000000..d43bb44
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/docs/semantics.md
@@ -0,0 +1,225 @@
+# FRP Semantics
+
+`Kairos`'s pure API is based off of the following denotational semantics
+([wikipedia](https://en.wikipedia.org/wiki/Denotational_semantics)).
+
+The semantics model `Kairos` types as time-varying values; by making `Time` a
+first-class value, we can define a referentially-transparent API that allows us
+to reason about the behavior of the pure `Kairos` combinators. This is
+implementation-agnostic; we can compare the behavior of any implementation with
+expected behavior denoted by these semantics to identify bugs.
+
+The semantics are written in pseudo-Kotlin; places where we are deviating from
+real Kotlin are noted with comments.
+
+``` kotlin
+
+sealed class Time : Comparable<Time> {
+ object BigBang : Time()
+ data class At(time: BigDecimal) : Time()
+ object Infinity : Time()
+
+ override final fun compareTo(other: Time): Int =
+ when (this) {
+ BigBang -> if (other === BigBang) 0 else -1
+ is At -> when (other) {
+ BigBang -> 1
+ is At -> time.compareTo(other.time)
+ Infinity -> -1
+ }
+ Infinity -> if (other === Infinity) 0 else 1
+ }
+}
+
+typealias Transactional<T> = (Time) -> T
+
+typealias TFlow<T> = SortedMap<Time, T>
+
+private fun <T> SortedMap<Time, T>.pairwise(): List<Pair<Pair<Time, T>, Pair<Time<T>>>> =
+ // NOTE: pretend evaluation is lazy, so that error() doesn't immediately throw
+ (toList() + Pair(Time.Infinity, error("no value"))).zipWithNext()
+
+class TState<T> internal constructor(
+ internal val current: Transactional<T>,
+ val stateChanges: TFlow<T>,
+)
+
+val emptyTFlow: TFlow<Nothing> = emptyMap()
+
+fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> =
+ mapValues { (t, a) -> FrpTransactionScope(t).f(a) }
+
+fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> =
+ filter { (t, a) -> FrpTransactionScope(t).f(a) }
+
+fun <A> merge(
+ first: TFlow<A>,
+ second: TFlow<A>,
+ onCoincidence: Time.(A, A) -> A,
+): TFlow<A> =
+ first.toMutableMap().also { result ->
+ second.forEach { (t, a) ->
+ result.merge(t, a) { f, s ->
+ FrpTranscationScope(t).onCoincidence(f, a)
+ }
+ }
+ }.toSortedMap()
+
+fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
+ val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
+ stateChanges.dropWhile { (time, _) -> time < time0 }
+ val events =
+ truncated
+ .pairwise()
+ .flatMap { ((t0, sa), (t2, _)) ->
+ sa.filter { (t1, a) -> t0 < t1 && t1 <= t2 }
+ }
+ return events.toSortedMap()
+}
+
+fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
+ val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
+ stateChanges.dropWhile { (time, _) -> time < time0 }
+ val events =
+ truncated
+ .pairwise()
+ .flatMap { ((t0, sa), (t2, _)) ->
+ sa.filter { (t1, a) -> t0 <= t1 && t1 <= t2 }
+ }
+ return events.toSortedMap()
+}
+
+typealias GroupedTFlow<K, V> = TFlow<Map<K, V>>
+
+fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this
+
+fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> =
+ map { m -> m[k] }.filter { it != null }.map { it!! }
+
+fun <A, B> TState<A>.map(f: (A) -> B): TState<B> =
+ TState(
+ current = { t -> f(current.invoke(t)) },
+ stateChanges = stateChanges.map { f(it) },
+ )
+
+fun <A, B, C> TState<A>.combineWith(
+ other: TState<B>,
+ f: (A, B) -> C,
+): TState<C> =
+ TState(
+ current = { t -> f(current.invoke(t), other.current.invoke(t)) },
+ stateChanges = run {
+ val aChanges =
+ stateChanges
+ .map { a ->
+ val b = other.current.sample()
+ Triple(a, b, f(a, b))
+ }
+ val bChanges =
+ other
+ .stateChanges
+ .map { b ->
+ val a = current.sample()
+ Triple(a, b, f(a, b))
+ }
+ merge(aChanges, bChanges) { (a, _, _), (_, b, _) ->
+ Triple(a, b, f(a, b))
+ }
+ .map { (_, _, zipped) -> zipped }
+ },
+ )
+
+fun <A> TState<TState<A>>.flatten(): TState<A> {
+ val changes =
+ stateChanges
+ .pairwise()
+ .flatMap { ((t0, oldInner), (t2, _)) ->
+ val inWindow =
+ oldInner
+ .stateChanges
+ .filter { (t1, b) -> t0 <= t1 && t1 < t2 }
+ if (inWindow.firstOrNull()?.time != t0) {
+ listOf(Pair(t0, oldInner.current.invoke(t0))) + inWindow
+ } else {
+ inWindow
+ }
+ }
+ return TState(
+ current = { t -> current.invoke(t).current.invoke(t) },
+ stateChanges = changes.toSortedMap(),
+ )
+}
+
+open class FrpTranscationScope internal constructor(
+ internal val currentTime: Time,
+) {
+ val now: TFlow<Unit> =
+ sortedMapOf(currentTime to Unit)
+
+ fun <A> Transactional<A>.sample(): A =
+ invoke(currentTime)
+
+ fun <A> TState<A>.sample(): A =
+ current.sample()
+}
+
+class FrpStateScope internal constructor(
+ time: Time,
+ internal val stopTime: Time,
+): FrpTransactionScope(time) {
+
+ fun <A, B> TFlow<A>.fold(
+ initialValue: B,
+ f: FrpTransactionScope.(B, A) -> B,
+ ): TState<B> {
+ val truncated =
+ dropWhile { (t, _) -> t < currentTime }
+ .takeWhile { (t, _) -> t <= stopTime }
+ val folded =
+ truncated
+ .scan(Pair(currentTime, initialValue)) { (_, b) (t, a) ->
+ Pair(t, FrpTransactionScope(t).f(a, b))
+ }
+ val lookup = { t1 ->
+ folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue
+ }
+ return TState(lookup, folded.toSortedMap())
+ }
+
+ fun <A> TFlow<A>.hold(initialValue: A): TState<A> =
+ fold(initialValue) { _, a -> a }
+
+ fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ initialValues: Map<K, V>
+ ): TState<Map<K, V>> =
+ fold(initialValues) { patch, map ->
+ val eithers = patch.map { (k, v) ->
+ if (v is Just) Left(k to v.value) else Right(k)
+ }
+ val adds = eithers.filterIsInstance<Left>().map { it.left }
+ val removes = eithers.filterIsInstance<Right>().map { it.right }
+ val removed: Map<K, V> = map - removes.toSet()
+ val updated: Map<K, V> = removed + adds
+ updated
+ }
+
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: Map<K, TFlow<V>>,
+ ): TFlow<Map<K, V>> =
+ foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
+
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey(
+ transform: suspend FrpStateScope.(A) -> B,
+ ): TFlow<Map<K, Maybe<B>>> =
+ pairwise().map { ((t0, patch), (t1, _)) ->
+ patch.map { (k, ma) ->
+ ma.map { a ->
+ FrpStateScope(t0, t1).transform(a)
+ }
+ }
+ }
+ }
+
+}
+
+```
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
new file mode 100644
index 0000000..8bf3a43
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
@@ -0,0 +1,250 @@
+/*
+ * 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.just
+import com.android.systemui.kairos.util.none
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
+
+/**
+ * Returns a [TFlow] that emits the value sampled from the [Transactional] produced by each emission
+ * of the original [TFlow], within the same transaction of the original emission.
+ */
+fun <A> TFlow<Transactional<A>>.sampleTransactionals(): TFlow<A> = map { it.sample() }
+
+/** @see FrpTransactionScope.sample */
+fun <A, B, C> TFlow<A>.sample(
+ state: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+): TFlow<C> = map { transform(it, state.sample()) }
+
+/** @see FrpTransactionScope.sample */
+fun <A, B, C> TFlow<A>.sample(
+ transactional: Transactional<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+): TFlow<C> = map { transform(it, transactional.sample()) }
+
+/**
+ * Like [sample], but if [state] is changing at the time it is sampled ([stateChanges] 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
+ */
+fun <A, B, C> TFlow<A>.samplePromptly(
+ state: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+): TFlow<C> =
+ sample(state) { a, b -> These.thiz<Pair<A, B>, B>(a to b) }
+ .mergeWith(state.stateChanges.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 [TState] containing a map with a snapshot of the current state of each [TState] in the
+ * original map.
+ */
+fun <K, A> Map<K, TState<A>>.combineValues(): TState<Map<K, A>> =
+ asIterable()
+ .map { (k, state) -> state.map { v -> k to v } }
+ .combine()
+ .map { entries -> entries.toMap() }
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [TFlow]. [network] is needed to
+ * transactionally connect to / disconnect from the [TFlow] when collection starts/stops.
+ */
+fun <A> TFlow<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [TState]. [network] is needed to
+ * transactionally connect to / disconnect from the [TState] when collection starts/stops.
+ */
+fun <A> TState<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this
+ * [network], and then emits from the returned [TFlow].
+ *
+ * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up.
+ */
+@JvmName("flowSpecToColdConflatedFlow")
+fun <A> FrpSpec<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this
+ * [network], and then emits from the returned [TState].
+ *
+ * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up.
+ */
+@JvmName("stateSpecToColdConflatedFlow")
+fun <A> FrpSpec<TState<A>>.toColdConflatedFlow(network: FrpNetwork): 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 [TFlow].
+ */
+@JvmName("transactionalFlowToColdConflatedFlow")
+fun <A> Transactional<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): 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 [TState].
+ */
+@JvmName("transactionalStateToColdConflatedFlow")
+fun <A> Transactional<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [FrpStateful] in a new transaction in
+ * this [network], and then emits from the returned [TFlow].
+ *
+ * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up.
+ */
+@JvmName("statefulFlowToColdConflatedFlow")
+fun <A> FrpStateful<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): 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 [TState].
+ *
+ * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up.
+ */
+@JvmName("statefulStateToColdConflatedFlow")
+fun <A> FrpStateful<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
+
+/** Return a [TFlow] that emits from the original [TFlow] only when [state] is `true`. */
+fun <A> TFlow<A>.filter(state: TState<Boolean>): TFlow<A> = filter { state.sample() }
+
+private fun Iterable<Boolean>.allTrue() = all { it }
+
+private fun Iterable<Boolean>.anyTrue() = any { it }
+
+/** Returns a [TState] that is `true` only when all of [states] are `true`. */
+fun allOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.allTrue() }
+
+/** Returns a [TState] that is `true` when any of [states] are `true`. */
+fun anyOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.anyTrue() }
+
+/** Returns a [TState] containing the inverse of the Boolean held by the original [TState]. */
+fun not(state: TState<Boolean>): TState<Boolean> = state.mapCheapUnsafe { !it }
+
+/**
+ * Represents a modal FRP sub-network.
+ *
+ * When [enabled][enableMode], all network modifications are applied immediately to the FRP network.
+ * When the returned [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode,
+ * undoing all modifications in the process (any registered [observers][FrpBuildScope.observe] are
+ * unregistered, and any pending [side-effects][FrpBuildScope.effect] are cancelled).
+ *
+ * Use [compiledFrpSpec] to compile and stand-up a mode graph.
+ *
+ * @see FrpStatefulMode
+ */
+fun interface FrpBuildMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a
+ * new mode.
+ */
+ suspend fun FrpBuildScope.enableMode(): Pair<A, TFlow<FrpBuildMode<A>>>
+}
+
+/**
+ * Returns an [FrpSpec] that, when [applied][FrpBuildScope.applySpec], stands up a modal-transition
+ * graph starting with this [FrpBuildMode], automatically switching to new modes as they are
+ * produced.
+ *
+ * @see FrpBuildMode
+ */
+val <A> FrpBuildMode<A>.compiledFrpSpec: FrpSpec<TState<A>>
+ get() = frpSpec {
+ var modeChangeEvents by TFlowLoop<FrpBuildMode<A>>()
+ val activeMode: TState<Pair<A, TFlow<FrpBuildMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { frpSpec { enableMode() } } }
+ .holdLatestSpec(frpSpec { enableMode() })
+ modeChangeEvents =
+ activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch()
+ activeMode.map { it.first }
+ }
+
+/**
+ * Represents a modal FRP sub-network.
+ *
+ * When [enabled][enableMode], all state accumulation is immediately started. When the returned
+ * [TFlow] emits a [FrpBuildMode], 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 FrpBuildMode
+ */
+fun interface FrpStatefulMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a
+ * new mode.
+ */
+ suspend fun FrpStateScope.enableMode(): Pair<A, TFlow<FrpStatefulMode<A>>>
+}
+
+/**
+ * Returns an [FrpStateful] that, when [applied][FrpStateScope.applyStateful], stands up a
+ * modal-transition graph starting with this [FrpStatefulMode], automatically switching to new modes
+ * as they are produced.
+ *
+ * @see FrpBuildMode
+ */
+val <A> FrpStatefulMode<A>.compiledStateful: FrpStateful<TState<A>>
+ get() = statefully {
+ var modeChangeEvents by TFlowLoop<FrpStatefulMode<A>>()
+ val activeMode: TState<Pair<A, TFlow<FrpStatefulMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { statefully { enableMode() } } }
+ .holdLatestStateful(statefully { enableMode() })
+ modeChangeEvents =
+ activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch()
+ activeMode.map { it.first }
+ }
+
+/**
+ * Runs [spec] in this [FrpBuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns
+ * a [TState] that holds the result of the currently-active [FrpSpec].
+ */
+fun <A> FrpBuildScope.rebuildOn(rebuildSignal: TFlow<*>, spec: FrpSpec<A>): TState<A> =
+ rebuildSignal.map { spec }.holdLatestSpec(spec)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt
new file mode 100644
index 0000000..4de6deb
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt
@@ -0,0 +1,864 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.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 kotlin.coroutines.RestrictsSuspension
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.launch
+
+/** A function that modifies the FrpNetwork. */
+typealias FrpSpec<A> = suspend FrpBuildScope.() -> A
+
+/**
+ * Constructs an [FrpSpec]. The passed [block] will be invoked with an [FrpBuildScope] that can be
+ * used to perform network-building operations, including adding new inputs and outputs to the
+ * network, as well as all operations available in [FrpTransactionScope].
+ */
+@ExperimentalFrpApi
+@Suppress("NOTHING_TO_INLINE")
+inline fun <A> frpSpec(noinline block: suspend FrpBuildScope.() -> A): FrpSpec<A> = block
+
+/** Applies the [FrpSpec] within this [FrpBuildScope]. */
+@ExperimentalFrpApi
+inline operator fun <A> FrpBuildScope.invoke(block: FrpBuildScope.() -> A) = run(block)
+
+/** Operations that add inputs and outputs to an FRP network. */
+@ExperimentalFrpApi
+@RestrictsSuspension
+interface FrpBuildScope : FrpStateScope {
+
+ /** TODO: Javadoc */
+ @ExperimentalFrpApi
+ fun <R> deferredBuildScope(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R>
+
+ /** TODO: Javadoc */
+ @ExperimentalFrpApi fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of
+ * the original [TFlow].
+ *
+ * **NOTE:** This API does not [observe] the original [TFlow], meaning that unless the returned
+ * (or a downstream) [TFlow] is observed separately, [transform] will not be invoked, and no
+ * internal side-effects will occur.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B>
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely
+ * performed in reaction to the emission.
+ *
+ * Specifically, [block] is deferred to the end of the transaction, and is only actually
+ * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * tFlow.observe { effect { ... } }
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observe(
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ block: suspend FrpEffectScope.(A) -> Unit = {},
+ ): Job
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs]
+ * immediately.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>>
+
+ /**
+ * Creates an instance of a [TFlow] with elements that are from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
+ * provided [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ */
+ @ExperimentalFrpApi fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T>
+
+ /**
+ * Creates an instance of a [TFlow] with elements that are emitted from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
+ * provided [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *coalesced* into batches. When a value is
+ * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via
+ * [coalesce]. Once the batch is consumed by the frp network in the next transaction, the batch
+ * is reset back to [getInitialValue].
+ */
+ @ExperimentalFrpApi
+ fun <In, Out> coalescingTFlow(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+ ): TFlow<Out>
+
+ /**
+ * Creates a new [FrpBuildScope] that is a child of this one.
+ *
+ * This new scope can be manually cancelled via the returned [Job], or will be cancelled
+ * automatically when its parent is cancelled. Cancellation will unregister all
+ * [observers][observe] and cancel all scheduled [effects][effect].
+ *
+ * The return value from [block] can be accessed via the returned [FrpDeferredValue].
+ */
+ @ExperimentalFrpApi fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job>
+
+ // TODO: once we have context params, these can all become extensions:
+
+ /**
+ * Returns a [TFlow] containing the results of applying the given [transform] function to each
+ * value of the original [TFlow].
+ *
+ * Unlike [TFlow.map], [transform] can perform arbitrary asynchronous code. This code is run
+ * outside of the current FRP transaction; when [transform] returns, the returned value is
+ * emitted from the result [TFlow] in a new transaction.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * tflow.mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten()
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapAsyncLatest(transform: suspend (A) -> B): TFlow<B> =
+ mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten()
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ *
+ * @see observe
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ mapBuild(block).observe()
+
+ /**
+ * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
+ * [value of this TState][TState.sample], and will emit at the same rate as
+ * [TState.stateChanges].
+ *
+ * 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 [TState.sample].
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.toStateFlow(): StateFlow<A> {
+ val uninitialized = Any()
+ var initialValue: Any? = uninitialized
+ val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
+ deferredBuildScope {
+ initialValue = sample()
+ stateChanges.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 FRP transaction has completed."
+ )
+ }
+
+ return object : StateFlow<A> {
+ override val replayCache: List<A>
+ get() = innerStateFlow.replayCache.map(::getValue)
+
+ override val value: A
+ get() = getValue(innerStateFlow.value)
+
+ override suspend fun collect(collector: FlowCollector<A>): Nothing {
+ innerStateFlow.collect { collector.emit(getValue(it)) }
+ }
+ }
+ }
+
+ /**
+ * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current
+ * [value][TState.sample] of this [TState] followed by all [stateChanges].
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
+ val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
+ deferredBuildScope {
+ result.tryEmit(sample())
+ stateChanges.observe { a -> result.tryEmit(a) }
+ }
+ return result
+ }
+
+ /**
+ * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values
+ * whenever this [TFlow] emits.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
+ val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
+ observe { a -> result.tryEmit(a) }
+ return result
+ }
+
+ /**
+ * Returns a [TState] that holds onto the value returned by applying the most recently emitted
+ * [FrpSpec] from the original [TFlow], or the value returned by applying [initialSpec] if
+ * nothing has been emitted since it was constructed.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpSpec<A>>.holdLatestSpec(initialSpec: FrpSpec<A>): TState<A> {
+ val (changes: TFlow<A>, initApplied: FrpDeferredValue<A>) = applyLatestSpec(initialSpec)
+ return changes.holdDeferred(initApplied)
+ }
+
+ /**
+ * Returns a [TState] containing the value returned by applying the [FrpSpec] held by the
+ * original [TState].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<FrpSpec<A>>.applyLatestSpec(): TState<A> {
+ val (appliedChanges: TFlow<A>, init: FrpDeferredValue<A>) =
+ stateChanges.applyLatestSpec(frpSpec { sample().applySpec() })
+ return appliedChanges.holdDeferred(init)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpSpec<A>>.applyLatestSpec(): TFlow<A> = applyLatestSpec(frpSpec {}).first
+
+ /**
+ * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the
+ * original [TFlow] emits a value.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * When the original [TFlow] emits a new value, those changes are undone (any registered
+ * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.flatMapLatestBuild(
+ transform: suspend FrpBuildScope.(A) -> TFlow<B>
+ ): TFlow<B> = mapCheap { frpSpec { transform(it) } }.applyLatestSpec().flatten()
+
+ /**
+ * Returns a [TState] by applying [transform] to the value held by the original [TState].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * When the value held by the original [TState] changes, those changes are undone (any
+ * registered [observers][observe] are unregistered, and any pending [effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.flatMapLatestBuild(
+ transform: suspend FrpBuildScope.(A) -> TState<B>
+ ): TState<B> = mapLatestBuild { transform(it) }.flatten()
+
+ /**
+ * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
+ * [transform].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * When the value held by the original [TState] changes, those changes are undone (any
+ * registered [observers][observe] are unregistered, and any pending [effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TState<B> =
+ mapCheapUnsafe { frpSpec { transform(it) } }.applyLatestSpec()
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpec]
+ * immediately.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A : Any?, B> TFlow<FrpSpec<B>>.applyLatestSpec(
+ initialSpec: FrpSpec<A>
+ ): Pair<TFlow<B>, FrpDeferredValue<A>> {
+ val (flow, result) =
+ mapCheap { spec -> mapOf(Unit to just(spec)) }
+ .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
+ val outFlow: TFlow<B> =
+ flow.mapMaybe {
+ checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
+ }
+ val outInit: FrpDeferredValue<A> = deferredBuildScope {
+ val initResult: Map<Unit, A> = result.get()
+ check(Unit in initResult) {
+ "applyLatest: expected initial result, but none present in: $initResult"
+ }
+ @Suppress("UNCHECKED_CAST")
+ initResult.getOrDefault(Unit) { null } as A
+ }
+ return Pair(outFlow, outInit)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> =
+ mapCheap { frpSpec { transform(it) } }.applyLatestSpec()
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValue] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestBuild(
+ initialValue: A,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<B>, FrpDeferredValue<B>> =
+ mapLatestBuildDeferred(deferredOf(initialValue), transform)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValue] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestBuildDeferred(
+ initialValue: FrpDeferredValue<A>,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<B>, FrpDeferredValue<B>> =
+ mapCheap { frpSpec { transform(it) } }
+ .applyLatestSpec(initialSpec = frpSpec { transform(initialValue.get()) })
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs]
+ * immediately.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: Map<K, FrpSpec<B>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ numKeys: Int? = null
+ ): TFlow<Map<K, Maybe<A>>> =
+ applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the
+ * original [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey(
+ initialSpecs: FrpDeferredValue<Map<K, FrpSpec<A>>>,
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> {
+ val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys)
+ return changes.foldMapIncrementally(initialValues)
+ }
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the
+ * original [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey(
+ initialSpecs: Map<K, FrpSpec<A>> = emptyMap(),
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation 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
+ * previously-active [FrpBuildScope] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ initialValues: FrpDeferredValue<Map<K, A>>,
+ numKeys: Int? = null,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ map { patch -> patch.mapValues { (_, v) -> v.map { frpSpec { transform(it) } } } }
+ .applyLatestSpecForKey(
+ deferredBuildScope {
+ initialValues.get().mapValues { (_, v) -> frpSpec { transform(v) } }
+ },
+ numKeys = numKeys,
+ )
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation 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
+ * previously-active [FrpBuildScope] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ initialValues: Map<K, A>,
+ numKeys: Int? = null,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation 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
+ * previously-active [FrpBuildScope] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ numKeys: Int? = null,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): TFlow<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first
+
+ /** Returns a [Deferred] containing the next value to be emitted from this [TFlow]. */
+ @ExperimentalFrpApi
+ fun <R> TFlow<R>.nextDeferred(): Deferred<R> {
+ lateinit var next: CompletableDeferred<R>
+ val job = nextOnly().observe { next.complete(it) }
+ next = CompletableDeferred<R>(parent = job)
+ return next
+ }
+
+ /** Returns a [TState] that reflects the [StateFlow.value] of this [StateFlow]. */
+ @ExperimentalFrpApi
+ fun <A> StateFlow<A>.toTState(): TState<A> {
+ val initial = value
+ return tFlow { dropWhile { it == initial }.collect { emit(it) } }.hold(initial)
+ }
+
+ /** Returns a [TFlow] that emits whenever this [Flow] emits. */
+ @ExperimentalFrpApi fun <A> Flow<A>.toTFlow(): TFlow<A> = tFlow { collect { emit(it) } }
+
+ /**
+ * Shorthand for:
+ * ```kotlin
+ * flow.toTFlow().hold(initialValue)
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> Flow<A>.toTState(initialValue: A): TState<A> = toTFlow().hold(initialValue)
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ *
+ * With each invocation of [block], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ mapLatestBuild { block(it) }.observe()
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely
+ * performed in reaction to the emission.
+ *
+ * With each invocation of [block], running effects from the previous invocation are cancelled.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job {
+ var innerJob: Job? = null
+ return observeBuild {
+ innerJob?.cancel()
+ innerJob = effect { block(it) }
+ }
+ }
+
+ /**
+ * Invokes [block] with the value held by this [TState], allowing side-effects to be safely
+ * performed in reaction to the state changing.
+ *
+ * With each invocation of [block], running effects from the previous invocation are cancelled.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job =
+ launchScope {
+ var innerJob = effect { block(sample()) }
+ stateChanges.observeBuild {
+ innerJob.cancel()
+ innerJob = effect { block(it) }
+ }
+ }
+
+ /**
+ * Applies [block] to the value held by this [TState]. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ *
+ * [block] can perform modifications to the FRP network via its [FrpBuildScope] receiver. With
+ * each invocation of [block], changes from the previous invocation are undone (any registered
+ * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ launchScope {
+ var innerJob: Job = launchScope { block(sample()) }
+ stateChanges.observeBuild {
+ innerJob.cancel()
+ innerJob = launchScope { block(it) }
+ }
+ }
+
+ /** Applies the [FrpSpec] within this [FrpBuildScope]. */
+ @ExperimentalFrpApi suspend fun <A> FrpSpec<A>.applySpec(): A = this()
+
+ /**
+ * Applies the [FrpSpec] within this [FrpBuildScope], returning the result as an
+ * [FrpDeferredValue].
+ */
+ @ExperimentalFrpApi
+ fun <A> FrpSpec<A>.applySpecDeferred(): FrpDeferredValue<A> = deferredBuildScope { applySpec() }
+
+ /**
+ * Invokes [block] on the value held in this [TState]. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ launchScope {
+ block(sample())
+ stateChanges.observeBuild(block)
+ }
+
+ /**
+ * Invokes [block] with the current value of this [TState], re-invoking whenever it changes,
+ * allowing side-effects to be safely performed in reaction value changing.
+ *
+ * Specifically, [block] is deferred to the end of the transaction, and is only actually
+ * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * If the [TState] is changing within the *current* transaction (i.e. [stateChanges] is
+ * presently emitting) then [block] will be invoked for the first time with the new value;
+ * otherwise, it will be invoked with the [current][sample] value.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observe(block: suspend FrpEffectScope.(A) -> Unit = {}): Job =
+ now.map { sample() }.mergeWith(stateChanges) { _, new -> new }.observe { block(it) }
+}
+
+/**
+ * Returns a [TFlow] that emits the result of [block] once it completes. [block] is evaluated
+ * outside of the current FRP transaction; when it completes, the returned [TFlow] emits in a new
+ * transaction.
+ *
+ * Shorthand for:
+ * ```
+ * tFlow { emitter: MutableTFlow<A> ->
+ * val a = block()
+ * emitter.emit(a)
+ * }
+ * ```
+ */
+@ExperimentalFrpApi
+fun <A> FrpBuildScope.asyncTFlow(block: suspend () -> A): TFlow<A> =
+ tFlow {
+ // TODO: if block completes synchronously, it would be nice to emit within this
+ // transaction
+ emit(block())
+ }
+ .apply { observe() }
+
+/**
+ * Performs a side-effect in a safe manner w/r/t the current FRP transaction.
+ *
+ * Specifically, [block] is deferred to the end of the current transaction, and is only actually
+ * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * now.observe { block() }
+ * ```
+ */
+@ExperimentalFrpApi
+fun FrpBuildScope.effect(block: suspend FrpEffectScope.() -> Unit): Job = now.observe { block() }
+
+/**
+ * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine.
+ *
+ * This coroutine is not actually started until the *end* of the current FRP transaction. This is
+ * done because the current [FrpBuildScope] 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 { frpCoroutineScope.launch { block() } }
+ * ```
+ */
+@ExperimentalFrpApi
+fun FrpBuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+
+/**
+ * Launches [block] in a new coroutine, returning the result as a [Deferred].
+ *
+ * This coroutine is not actually started until the *end* of the current FRP transaction. This is
+ * done because the current [FrpBuildScope] 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
+ * CompletableDeferred<R>.apply {
+ * effect { frpCoroutineScope.launch { complete(coroutineScope { block() }) } }
+ * }
+ * .await()
+ * ```
+ */
+@ExperimentalFrpApi
+fun <R> FrpBuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+ val result = CompletableDeferred<R>()
+ val job = now.observe { frpCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+ val handle = job.invokeOnCompletion { result.cancel() }
+ result.invokeOnCompletion {
+ handle.dispose()
+ job.cancel()
+ }
+ return result
+}
+
+/** Like [FrpBuildScope.asyncScope], but ignores the result of [block]. */
+@ExperimentalFrpApi fun FrpBuildScope.launchScope(block: FrpSpec<*>): Job = asyncScope(block).second
+
+/**
+ * Creates an instance of a [TFlow] with elements that are emitted from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided
+ * [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op
+ * observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *coalesced* into batches. When a value is
+ * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via
+ * [coalesce]. Once the batch is consumed by the FRP network in the next transaction, the batch is
+ * reset back to [initialValue].
+ */
+@ExperimentalFrpApi
+fun <In, Out> FrpBuildScope.coalescingTFlow(
+ initialValue: Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+): TFlow<Out> = coalescingTFlow(getInitialValue = { initialValue }, coalesce, builder)
+
+/**
+ * Creates an instance of a [TFlow] with elements that are emitted from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided
+ * [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op
+ * observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only
+ * the most recent emission will be used when the FRP network is ready.
+ */
+@ExperimentalFrpApi
+fun <T> FrpBuildScope.conflatedTFlow(
+ builder: suspend FrpCoalescingProducerScope<T>.() -> Unit
+): TFlow<T> =
+ coalescingTFlow<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder)
+ .mapCheap {
+ @Suppress("UNCHECKED_CAST")
+ it as T
+ }
+
+/** Scope for emitting to a [FrpBuildScope.coalescingTFlow]. */
+interface FrpCoalescingProducerScope<in T> {
+ /**
+ * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not
+ * already pending.
+ *
+ * Backpressure occurs when [emit] is called while the FRP network is currently in a
+ * transaction; if called multiple times, then emissions will be coalesced into a single batch
+ * that is then processed when the network is ready.
+ */
+ fun emit(value: T)
+}
+
+/** Scope for emitting to a [FrpBuildScope.tFlow]. */
+interface FrpProducerScope<in T> {
+ /**
+ * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing
+ * the emission has completed.
+ */
+ suspend fun emit(value: T)
+}
+
+/**
+ * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of
+ * [FrpBuildScope.tFlow] and [FrpBuildScope.coalescingTFlow].
+ */
+suspend fun awaitClose(block: () -> Unit): Nothing =
+ try {
+ awaitCancellation()
+ } finally {
+ block()
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt
new file mode 100644
index 0000000..be2eb43
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt
@@ -0,0 +1,48 @@
+/*
+ * 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 kotlin.coroutines.RestrictsSuspension
+import kotlinx.coroutines.CoroutineScope
+
+/**
+ * Scope for external side-effects triggered by the Frp network. This still occurs within the
+ * context of a transaction, so general suspending calls are disallowed to prevent blocking the
+ * transaction. You can use [frpCoroutineScope] to [launch] new coroutines to perform long-running
+ * asynchronous work. This scope is alive for the duration of the containing [FrpBuildScope] that
+ * this side-effect scope is running in.
+ */
+@RestrictsSuspension
+@ExperimentalFrpApi
+interface FrpEffectScope : FrpTransactionScope {
+ /**
+ * A [CoroutineScope] whose lifecycle lives for as long as this [FrpEffectScope] is alive. This
+ * is generally until the [Job] returned by [FrpBuildScope.effect] is cancelled.
+ */
+ @ExperimentalFrpApi val frpCoroutineScope: CoroutineScope
+
+ /**
+ * A [FrpNetwork] instance that can be used to transactionally query / modify the FRP network.
+ *
+ * The lambda passed to [FrpNetwork.transact] on this instance will receive an [FrpBuildScope]
+ * that is lifetime-bound to this [FrpEffectScope]. Once this [FrpEffectScope] is no longer
+ * alive, any modifications to the FRP network performed via this [FrpNetwork] instance will be
+ * undone (any registered [observers][FrpBuildScope.observe] are unregistered, and any pending
+ * [side-effects][FrpBuildScope.effect] are cancelled).
+ */
+ @ExperimentalFrpApi val frpNetwork: FrpNetwork
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt
new file mode 100644
index 0000000..b688eaf
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.internal.BuildScopeImpl
+import com.android.systemui.kairos.internal.Network
+import com.android.systemui.kairos.internal.StateScopeImpl
+import com.android.systemui.kairos.internal.util.awaitCancellationAndThen
+import com.android.systemui.kairos.internal.util.childScope
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+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.
+ */
+@RequiresOptIn(
+ message = "This API is experimental and should not be used in general production code."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalFrpApi
+
+/**
+ * External interface to an FRP network. Can be used to make transactional queries and modifications
+ * to the network.
+ */
+@ExperimentalFrpApi
+interface FrpNetwork {
+ /**
+ * Runs [block] inside of a transaction, suspending until the transaction is complete.
+ *
+ * The [FrpBuildScope] receiver exposes methods that can be used to query or modify the network.
+ * If the network is cancelled while the caller of [transact] is suspended, then the call will
+ * be cancelled.
+ */
+ @ExperimentalFrpApi suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R
+
+ /**
+ * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers
+ * and long-running effects are kept alive. When cancelled, observers are unregistered and
+ * effects are cancelled.
+ */
+ @ExperimentalFrpApi suspend fun activateSpec(spec: FrpSpec<*>)
+
+ /** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+ @ExperimentalFrpApi
+ fun <In, Out> coalescingMutableTFlow(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+ ): CoalescingMutableTFlow<In, Out>
+
+ /** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */
+ @ExperimentalFrpApi fun <T> mutableTFlow(): MutableTFlow<T>
+
+ /** Returns a [MutableTState]. with initial state [initialValue]. */
+ @ExperimentalFrpApi
+ fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T>
+}
+
+/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <In, Out> FrpNetwork.coalescingMutableTFlow(
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableTFlow<In, Out> =
+ coalescingMutableTFlow(coalesce, getInitialValue = { initialValue })
+
+/** Returns a [MutableTState]. with initial state [initialValue]. */
+@ExperimentalFrpApi
+fun <T> FrpNetwork.mutableTState(initialValue: T): MutableTState<T> =
+ mutableTStateDeferred(deferredOf(initialValue))
+
+/** Returns a [MutableTState]. with initial state [initialValue]. */
+@ExperimentalFrpApi
+fun <T> MutableTState(network: FrpNetwork, initialValue: T): MutableTState<T> =
+ network.mutableTState(initialValue)
+
+/** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <T> MutableTFlow(network: FrpNetwork): MutableTFlow<T> = network.mutableTFlow()
+
+/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <In, Out> CoalescingMutableTFlow(
+ network: FrpNetwork,
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce) { initialValue }
+
+/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <In, Out> CoalescingMutableTFlow(
+ network: FrpNetwork,
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce, getInitialValue)
+
+/**
+ * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely.
+ * While suspended, all observers and long-running effects are kept alive. When cancelled, observers
+ * are unregistered and effects are cancelled.
+ */
+@ExperimentalFrpApi
+suspend fun <R> FrpNetwork.activateSpec(spec: FrpSpec<R>, block: suspend (R) -> Unit) {
+ activateSpec {
+ val result = spec.applySpec()
+ launchEffect { block(result) }
+ }
+}
+
+internal class LocalFrpNetwork(
+ private val network: Network,
+ private val scope: CoroutineScope,
+ private val endSignal: TFlow<Any>,
+) : FrpNetwork {
+ override suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R {
+ val result = CompletableDeferred<R>(coroutineContext[Job])
+ @Suppress("DeferredResultUnused")
+ network.transaction {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal),
+ coroutineScope = scope,
+ )
+ buildScope.runInBuildScope { effect { result.complete(block()) } }
+ }
+ return result.await()
+ }
+
+ override suspend fun activateSpec(spec: FrpSpec<*>) {
+ val job =
+ network
+ .transaction {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal),
+ coroutineScope = scope,
+ )
+ buildScope.runInBuildScope { launchScope(spec) }
+ }
+ .await()
+ awaitCancellationAndThen { job.cancel() }
+ }
+
+ override fun <In, Out> coalescingMutableTFlow(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+ ): CoalescingMutableTFlow<In, Out> = CoalescingMutableTFlow(coalesce, network, getInitialValue)
+
+ override fun <T> mutableTFlow(): MutableTFlow<T> = MutableTFlow(network)
+
+ override fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> =
+ MutableTState(network, initialValue.unwrapped)
+}
+
+/**
+ * Combination of an [FrpNetwork] and a [Job] that, when cancelled, will cancel the entire FRP
+ * network.
+ */
+@ExperimentalFrpApi
+class RootFrpNetwork
+internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) :
+ Job by job, FrpNetwork by LocalFrpNetwork(network, scope, emptyTFlow)
+
+/** Constructs a new [RootFrpNetwork] in the given [CoroutineScope]. */
+@ExperimentalFrpApi
+fun CoroutineScope.newFrpNetwork(
+ context: CoroutineContext = EmptyCoroutineContext
+): RootFrpNetwork {
+ val scope = childScope(context)
+ val network = Network(scope)
+ scope.launch(CoroutineName("newFrpNetwork scheduler")) { network.runInputScheduler() }
+ return RootFrpNetwork(network, scope, scope.coroutineContext.job)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt
new file mode 100644
index 0000000..ad6b2c8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt
@@ -0,0 +1,73 @@
+/*
+ * 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 kotlin.coroutines.RestrictsSuspension
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/** Denotes [FrpScope] interfaces as [DSL markers][DslMarker]. */
+@DslMarker annotation class FrpScopeMarker
+
+/**
+ * Base scope for all FRP scopes. Used to prevent implicitly capturing other scopes from in lambdas.
+ */
+@FrpScopeMarker
+@RestrictsSuspension
+@ExperimentalFrpApi
+interface FrpScope {
+ /**
+ * Returns the value held by the [FrpDeferredValue], suspending until available if necessary.
+ */
+ @ExperimentalFrpApi
+ @OptIn(ExperimentalCoroutinesApi::class)
+ suspend fun <A> FrpDeferredValue<A>.get(): A = suspendCancellableCoroutine { k ->
+ unwrapped.invokeOnCompletion { ex ->
+ ex?.let { k.resumeWithException(ex) } ?: k.resume(unwrapped.getCompleted())
+ }
+ }
+}
+
+/**
+ * A value that may not be immediately (synchronously) available, but is guaranteed to be available
+ * before this transaction is completed.
+ *
+ * @see FrpScope.get
+ */
+@ExperimentalFrpApi
+class FrpDeferredValue<out A> internal constructor(internal val unwrapped: Deferred<A>)
+
+/**
+ * Returns the value held by this [FrpDeferredValue], or throws [IllegalStateException] if it is not
+ * yet available.
+ *
+ * This API is not meant for general usage within the FRP network. It is made available mainly for
+ * debugging and logging. You should always prefer [get][FrpScope.get] if possible.
+ *
+ * @see FrpScope.get
+ */
+@ExperimentalFrpApi
+@OptIn(ExperimentalCoroutinesApi::class)
+fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.getCompleted()
+
+/** Returns an already-available [FrpDeferredValue] containing [value]. */
+@ExperimentalFrpApi
+fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableDeferred(value))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt
new file mode 100644
index 0000000..c7ea680
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt
@@ -0,0 +1,780 @@
+/*
+ * 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.combine as combinePure
+import com.android.systemui.kairos.map as mapPure
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.Right
+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.none
+import com.android.systemui.kairos.util.partitionEithers
+import com.android.systemui.kairos.util.zipWith
+import kotlin.coroutines.RestrictsSuspension
+
+typealias FrpStateful<R> = suspend FrpStateScope.() -> R
+
+/**
+ * Returns a [FrpStateful] that, when [applied][FrpStateScope.applyStateful], invokes [block] with
+ * the applier's [FrpStateScope].
+ */
+// TODO: caching story? should each Scope have a cache of applied FrpStateful instances?
+@ExperimentalFrpApi
+@Suppress("NOTHING_TO_INLINE")
+inline fun <A> statefully(noinline block: suspend FrpStateScope.() -> A): FrpStateful<A> = block
+
+/**
+ * Operations that accumulate state within the FRP network.
+ *
+ * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as
+ * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running
+ * longer than needed.
+ */
+@ExperimentalFrpApi
+@RestrictsSuspension
+interface FrpStateScope : FrpTransactionScope {
+
+ /** TODO */
+ @ExperimentalFrpApi
+ // TODO: wish this could just be `deferred` but alas
+ fun <A> deferredStateScope(block: suspend FrpStateScope.() -> A): FrpDeferredValue<A>
+
+ /**
+ * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or
+ * [initialValue] if nothing has been emitted since it was constructed.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A>
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>>
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>>
+
+ // TODO: everything below this comment can be made into extensions once we have context params
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: Map<K, TFlow<V>> = emptyMap()
+ ): TFlow<Map<K, V>> = mergeIncrementally(deferredOf(initialTFlows))
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
+ initialTFlows: Map<K, TFlow<V>> = emptyMap()
+ ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows))
+
+ /** Applies the [FrpStateful] within this [FrpStateScope]. */
+ @ExperimentalFrpApi suspend fun <A> FrpStateful<A>.applyStateful(): A = this()
+
+ /**
+ * Applies the [FrpStateful] within this [FrpStateScope], returning the result as an
+ * [FrpDeferredValue].
+ */
+ @ExperimentalFrpApi
+ fun <A> FrpStateful<A>.applyStatefulDeferred(): FrpDeferredValue<A> = deferredStateScope {
+ applyStateful()
+ }
+
+ /**
+ * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or
+ * [initialValue] if nothing has been emitted since it was constructed.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.hold(initialValue: A): TState<A> = holdDeferred(deferredOf(initialValue))
+
+ /**
+ * Returns a [TFlow] the emits the result of applying [FrpStatefuls][FrpStateful] emitted from
+ * the original [TFlow].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
+ * of the original [TFlow].
+ */
+ @ExperimentalFrpApi fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A>
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. Unlike
+ * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
+ * original [TFlow].
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> =
+ mapPure { statefully { transform(it) } }.applyStatefuls()
+
+ /**
+ * Returns a [TState] the holds the result of applying the [FrpStateful] held by the original
+ * [TState].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<FrpStateful<A>>.applyStatefuls(): TState<A> =
+ stateChanges
+ .applyStatefuls()
+ .holdDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+
+ /** Returns a [TFlow] that switches to the [TFlow] emitted by the original [TFlow]. */
+ @ExperimentalFrpApi fun <A> TFlow<TFlow<A>>.flatten() = hold(emptyTFlow).switch()
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> =
+ mapPure { statefully { transform(it) } }.applyLatestStateful()
+
+ /**
+ * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the
+ * original [TFlow] emits a value.
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.flatMapLatestStateful(
+ transform: suspend FrpStateScope.(A) -> TFlow<B>
+ ): TFlow<B> = mapLatestStateful(transform).flatten()
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpStateful<A>>.applyLatestStateful(): TFlow<A> = applyLatestStateful {}.first
+
+ /**
+ * Returns a [TState] containing the value returned by applying the [FrpStateful] held by the
+ * original [TState].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<FrpStateful<A>>.applyLatestStateful(): TState<A> {
+ val (changes, init) = stateChanges.applyLatestStateful { sample()() }
+ return changes.holdDeferred(init)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<FrpStateful<B>>.applyLatestStateful(
+ init: FrpStateful<A>
+ ): Pair<TFlow<B>, FrpDeferredValue<A>> {
+ val (flow, result) =
+ mapCheap { spec -> mapOf(Unit to just(spec)) }
+ .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
+ val outFlow: TFlow<B> =
+ flow.mapMaybe {
+ checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
+ }
+ val outInit: FrpDeferredValue<A> = deferredTransactionScope {
+ val initResult: Map<Unit, A> = result.get()
+ check(Unit in initResult) {
+ "applyLatest: expected initial result, but none present in: $initResult"
+ }
+ @Suppress("UNCHECKED_CAST")
+ initResult.getOrDefault(Unit) { null } as A
+ }
+ return Pair(outFlow, outInit)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>>
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ init: Map<K, FrpStateful<B>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestStatefulForKey(deferredOf(init), numKeys)
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from
+ * the original [TFlow].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey(
+ init: FrpDeferredValue<Map<K, FrpStateful<A>>>,
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> {
+ val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys)
+ return changes.foldMapIncrementally(initialValues)
+ }
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from
+ * the original [TFlow].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey(
+ init: Map<K, FrpStateful<A>> = emptyMap(),
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys)
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ numKeys: Int? = null
+ ): TFlow<Map<K, Maybe<A>>> =
+ applyLatestStatefulForKey(init = emptyMap<K, FrpStateful<*>>(), numKeys = numKeys).first
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] 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 [FrpStateScope] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ initialValues: FrpDeferredValue<Map<K, A>>,
+ numKeys: Int? = null,
+ transform: suspend FrpStateScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ mapPure { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
+ .applyLatestStatefulForKey(
+ deferredStateScope {
+ initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+ },
+ numKeys = numKeys,
+ )
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] 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 [FrpStateScope] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ initialValues: Map<K, A>,
+ numKeys: Int? = null,
+ transform: suspend FrpStateScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] 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 [FrpStateScope] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ numKeys: Int? = null,
+ transform: suspend FrpStateScope.(A) -> B,
+ ): TFlow<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+
+ /**
+ * Returns a [TFlow] that will only emit the next event of the original [TFlow], and then will
+ * act as [emptyTFlow].
+ *
+ * If the original [TFlow] is emitting an event at this exact time, then it will be the only
+ * even emitted from the result [TFlow].
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.nextOnly(): TFlow<A> =
+ if (this === emptyTFlow) {
+ this
+ } else {
+ TFlowLoop<A>().also {
+ it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch()
+ }
+ }
+
+ /** Returns a [TFlow] that skips the next emission of the original [TFlow]. */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.skipNext(): TFlow<A> =
+ if (this === emptyTFlow) {
+ this
+ } else {
+ nextOnly().mapCheap { this@skipNext }.hold(emptyTFlow).switch()
+ }
+
+ /**
+ * Returns a [TFlow] that emits values from the original [TFlow] up until [stop] emits a value.
+ *
+ * If the original [TFlow] emits at the same time as [stop], then the returned [TFlow] will emit
+ * that value.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.takeUntil(stop: TFlow<*>): TFlow<A> =
+ if (stop === emptyTFlow) {
+ this
+ } else {
+ stop.mapCheap { emptyTFlow }.nextOnly().hold(this).switch()
+ }
+
+ /**
+ * Invokes [stateful] in a new [FrpStateScope] that is a child of this one.
+ *
+ * This new scope is stopped when [stop] first emits a value, or when the parent scope is
+ * stopped. Stopping will end all state accumulation; any [TStates][TState] returned from this
+ * scope will no longer update.
+ */
+ @ExperimentalFrpApi
+ fun <A> childStateScope(stop: TFlow<*>, stateful: FrpStateful<A>): FrpDeferredValue<A> {
+ val (_, init: FrpDeferredValue<Map<Unit, A>>) =
+ stop
+ .nextOnly()
+ .mapPure { mapOf(Unit to none<FrpStateful<A>>()) }
+ .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
+ return deferredStateScope { init.get().getValue(Unit) }
+ }
+
+ /**
+ * Returns a [TFlow] that emits values from the original [TFlow] up to and including a value is
+ * emitted that satisfies [predicate].
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.takeUntil(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> =
+ takeUntil(filter(predicate))
+
+ /**
+ * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying
+ * [transform] to both the emitted value and the currently tracked state.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.fold(
+ initialValue: B,
+ transform: suspend FrpTransactionScope.(A, B) -> B,
+ ): TState<B> {
+ lateinit var state: TState<B>
+ return mapPure { a -> transform(a, state.sample()) }.hold(initialValue).also { state = it }
+ }
+
+ /**
+ * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying
+ * [transform] to both the emitted value and the currently tracked state.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.foldDeferred(
+ initialValue: FrpDeferredValue<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> B,
+ ): TState<B> {
+ lateinit var state: TState<B>
+ return mapPure { a -> transform(a, state.sample()) }
+ .holdDeferred(initialValue)
+ .also { state = it }
+ }
+
+ /**
+ * Returns a [TState] that holds onto the result of applying the most recently emitted
+ * [FrpStateful] this [TFlow], or [init] if nothing has been emitted since it was constructed.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * val (changes, initApplied) = applyLatestStateful(init)
+ * return changes.toTStateDeferred(initApplied)
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpStateful<A>>.holdLatestStateful(init: FrpStateful<A>): TState<A> {
+ val (changes, initApplied) = applyLatestStateful(init)
+ return changes.holdDeferred(initApplied)
+ }
+
+ /**
+ * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow].
+ * [initialValue] is used as the previous value for the first emission.
+ *
+ * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
+ */
+ @ExperimentalFrpApi
+ fun <S, T : S> TFlow<T>.pairwise(initialValue: S): TFlow<WithPrev<S, T>> {
+ val previous = hold(initialValue)
+ return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) }
+ }
+
+ /**
+ * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. Note
+ * that the returned [TFlow] will not emit until the original [TFlow] has emitted twice.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.pairwise(): TFlow<WithPrev<A, A>> =
+ mapCheap { just(it) }
+ .pairwise(none)
+ .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
+
+ /**
+ * Returns a [TState] that holds both the current and previous values of the original [TState].
+ * [initialPreviousValue] is used as the first previous value.
+ *
+ * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
+ */
+ @ExperimentalFrpApi
+ fun <S, T : S> TState<T>.pairwise(initialPreviousValue: S): TState<WithPrev<S, T>> =
+ stateChanges
+ .pairwise(initialPreviousValue)
+ .holdDeferred(deferredTransactionScope { WithPrev(initialPreviousValue, sample()) })
+
+ /**
+ * Returns a [TState] 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].
+ */
+ @ExperimentalFrpApi
+ fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ initialValues: FrpDeferredValue<Map<K, V>>
+ ): TState<Map<K, V>> =
+ foldDeferred(initialValues) { patch, map ->
+ 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) }
+ .partitionEithers()
+ val removed: Map<K, V> = map - removes.toSet()
+ val updated: Map<K, V> = removed + adds
+ updated
+ }
+
+ /**
+ * Returns a [TState] 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].
+ */
+ @ExperimentalFrpApi
+ fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ initialValues: Map<K, V> = emptyMap()
+ ): TState<Map<K, V>> = foldMapIncrementally(deferredOf(initialValues))
+
+ /**
+ * Returns a [TFlow] that wraps each emission of the original [TFlow] 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) }
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.withIndex(): TFlow<IndexedValue<A>> {
+ val index = fold(0) { _, old -> old + 1 }
+ return sample(index) { a, idx -> IndexedValue(idx, a) }
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow] and its index (starting from zero).
+ *
+ * Shorthand for:
+ * ```
+ * withIndex().map { (idx, a) -> transform(idx, a) }
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapIndexed(transform: suspend FrpTransactionScope.(Int, A) -> B): TFlow<B> {
+ val index = fold(0) { _, i -> i + 1 }
+ return sample(index) { a, idx -> transform(idx, a) }
+ }
+
+ /** Returns a [TFlow] where all subsequent repetitions of the same value are filtered out. */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.distinctUntilChanged(): TFlow<A> {
+ val state: TState<Any?> = hold(Any())
+ return filter { it != state.sample() }
+ }
+
+ /**
+ * Returns a new [TFlow] that emits at the same rate as the original [TFlow], but combines the
+ * emitted value with the most recent emission from [other] using [transform].
+ *
+ * Note that the returned [TFlow] will not emit anything until [other] has emitted at least one
+ * value.
+ */
+ @ExperimentalFrpApi
+ fun <A, B, C> TFlow<A>.sample(
+ other: TFlow<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+ ): TFlow<C> {
+ val state = other.mapCheap { just(it) }.hold(none)
+ return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
+ }
+
+ /**
+ * Returns a [TState] that samples the [Transactional] held by the given [TState] within the
+ * same transaction that the state changes.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<Transactional<A>>.sampleTransactionals(): TState<A> =
+ stateChanges
+ .sampleTransactionals()
+ .holdDeferred(deferredTransactionScope { sample().sample() })
+
+ /**
+ * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
+ * given function [transform].
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TState<B> =
+ mapPure { transactionally { transform(it) } }.sampleTransactionals()
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, B, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> Z,
+ ): TState<Z> =
+ com.android.systemui.kairos
+ .combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
+ .sampleTransactionals()
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, B, C, D, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ stateC: TState<C>,
+ stateD: TState<D>,
+ transform: suspend FrpTransactionScope.(A, B, C, D) -> Z,
+ ): TState<Z> =
+ com.android.systemui.kairos
+ .combine(stateA, stateB, stateC, stateD) { a, b, c, d ->
+ transactionally { transform(a, b, c, d) }
+ }
+ .sampleTransactionals()
+
+ /** Returns a [TState] by applying [transform] to the value held by the original [TState]. */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.flatMap(
+ transform: suspend FrpTransactionScope.(A) -> TState<B>
+ ): TState<B> = mapPure { transactionally { transform(it) } }.sampleTransactionals().flatten()
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, Z> combine(
+ vararg states: TState<A>,
+ transform: suspend FrpTransactionScope.(List<A>) -> Z,
+ ): TState<Z> = combinePure(*states).map(transform)
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, Z> Iterable<TState<A>>.combine(
+ transform: suspend FrpTransactionScope.(List<A>) -> Z
+ ): TState<Z> = combinePure().map(transform)
+
+ /**
+ * Returns a [TState] by combining the values held inside the given [TState]s by applying them
+ * to the given function [transform].
+ */
+ @ExperimentalFrpApi
+ fun <A, B, C> TState<A>.combineWith(
+ other: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+ ): TState<C> = combine(this, other, transform)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt
new file mode 100644
index 0000000..a7ae1d9
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 kotlin.coroutines.RestrictsSuspension
+
+/**
+ * FRP operations that are available while a transaction is active.
+ *
+ * These operations do not accumulate state, which makes [FrpTransactionScope] weaker than
+ * [FrpStateScope], but allows them to be used in more places.
+ */
+@ExperimentalFrpApi
+@RestrictsSuspension
+interface FrpTransactionScope : FrpScope {
+
+ /**
+ * Returns the current value of this [Transactional] as a [FrpDeferredValue].
+ *
+ * @see sample
+ */
+ @ExperimentalFrpApi fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A>
+
+ /**
+ * Returns the current value of this [TState] as a [FrpDeferredValue].
+ *
+ * @see sample
+ */
+ @ExperimentalFrpApi fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A>
+
+ /** TODO */
+ @ExperimentalFrpApi
+ fun <A> deferredTransactionScope(
+ block: suspend FrpTransactionScope.() -> A
+ ): FrpDeferredValue<A>
+
+ /** A [TFlow] that emits once, within this transaction, and then never again. */
+ @ExperimentalFrpApi val now: TFlow<Unit>
+
+ /**
+ * Returns the current value held by this [TState]. Guaranteed to be consistent within the same
+ * transaction.
+ */
+ @ExperimentalFrpApi suspend fun <A> TState<A>.sample(): A = sampleDeferred().get()
+
+ /**
+ * Returns the current value held by this [Transactional]. Guaranteed to be consistent within
+ * the same transaction.
+ */
+ @ExperimentalFrpApi suspend fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt
new file mode 100644
index 0000000..7ba1aca
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt
@@ -0,0 +1,560 @@
+/*
+ * 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.internal.DemuxImpl
+import com.android.systemui.kairos.internal.Init
+import com.android.systemui.kairos.internal.InitScope
+import com.android.systemui.kairos.internal.InputNode
+import com.android.systemui.kairos.internal.Network
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.TFlowImpl
+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.filterNode
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.map
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.mapMaybeNode
+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.switchPromptImpl
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.Right
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.map
+import com.android.systemui.kairos.util.toMaybe
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+/** A series of values of type [A] available at discrete points in time. */
+@ExperimentalFrpApi
+sealed class TFlow<out A> {
+ companion object {
+ /** A [TFlow] with no values. */
+ val empty: TFlow<Nothing> = EmptyFlow
+ }
+}
+
+/** A [TFlow] with no values. */
+@ExperimentalFrpApi val emptyTFlow: TFlow<Nothing> = TFlow.empty
+
+/**
+ * A forward-reference to a [TFlow]. Useful for recursive definitions.
+ *
+ * This reference can be used like a standard [TFlow], but will hold up evaluation of the FRP
+ * network until the [loopback] reference is set.
+ */
+@ExperimentalFrpApi
+class TFlowLoop<A> : TFlow<A>() {
+ private val deferred = CompletableDeferred<TFlow<A>>()
+
+ internal val init: Init<TFlowImpl<A>> =
+ init(name = null) { deferred.await().init.connect(evalScope = this) }
+
+ /** The [TFlow] this reference is referring to. */
+ @ExperimentalFrpApi
+ var loopback: TFlow<A>? = null
+ set(value) {
+ value?.let {
+ check(deferred.complete(value)) { "TFlowLoop.loopback has already been set." }
+ field = value
+ }
+ }
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): TFlow<A> = this
+
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TFlow<A>) {
+ loopback = value
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+/** TODO */
+@ExperimentalFrpApi fun <A> Lazy<TFlow<A>>.defer(): TFlow<A> = deferInline { value }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> FrpDeferredValue<TFlow<A>>.defer(): TFlow<A> = deferInline { unwrapped.await() }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> deferTFlow(block: suspend FrpScope.() -> TFlow<A>): TFlow<A> = deferInline {
+ NoScope.runInFrpScope(block)
+}
+
+/** Returns a [TFlow] that emits the new value of this [TState] when it changes. */
+@ExperimentalFrpApi
+val <A> TState<A>.stateChanges: TFlow<A>
+ get() = TFlowInit(init(name = null) { init.connect(evalScope = this).changes })
+
+/**
+ * Returns a [TFlow] that contains only the [just] results of applying [transform] to each value of
+ * the original [TFlow].
+ *
+ * @see mapNotNull
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.mapMaybe(transform: suspend FrpTransactionScope.(A) -> Maybe<B>): TFlow<B> {
+ val pulse =
+ mapMaybeNode({ init.connect(evalScope = this) }) { runInTransactionScope { transform(it) } }
+ return TFlowInit(constInit(name = null, pulse))
+}
+
+/**
+ * Returns a [TFlow] that contains only the non-null results of applying [transform] to each value
+ * of the original [TFlow].
+ *
+ * @see mapMaybe
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.mapNotNull(transform: suspend FrpTransactionScope.(A) -> B?): TFlow<B> =
+ mapMaybe {
+ transform(it).toMaybe()
+ }
+
+/** Returns a [TFlow] containing only values of the original [TFlow] that are not null. */
+@ExperimentalFrpApi fun <A> TFlow<A?>.filterNotNull(): TFlow<A> = mapNotNull { it }
+
+/** Shorthand for `mapNotNull { it as? A }`. */
+@ExperimentalFrpApi
+inline fun <reified A> TFlow<*>.filterIsInstance(): TFlow<A> = mapNotNull { it as? A }
+
+/** Shorthand for `mapMaybe { it }`. */
+@ExperimentalFrpApi fun <A> TFlow<Maybe<A>>.filterJust(): TFlow<A> = mapMaybe { it }
+
+/**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the original
+ * [TFlow].
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> {
+ val mapped: TFlowImpl<B> =
+ mapImpl({ init.connect(evalScope = this) }) { a -> runInTransactionScope { transform(a) } }
+ return TFlowInit(constInit(name = null, mapped.cached()))
+}
+
+/**
+ * Like [map], but the emission is not cached during the transaction. Use only if [transform] is
+ * fast and pure.
+ *
+ * @see map
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.mapCheap(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> =
+ TFlowInit(
+ constInit(
+ name = null,
+ mapImpl({ init.connect(evalScope = this) }) { a ->
+ runInTransactionScope { transform(a) }
+ },
+ )
+ )
+
+/**
+ * Returns a [TFlow] that invokes [action] before each value of the original [TFlow] is emitted.
+ * Useful for logging and debugging.
+ *
+ * ```
+ * pulse.onEach { foo(it) } == pulse.map { foo(it); it }
+ * ```
+ *
+ * Note that the side effects performed in [onEach] are only performed while the resulting [TFlow]
+ * is connected to an output of the FRP network. If your goal is to reliably perform side effects in
+ * response to a [TFlow], use the output combinators available in [FrpBuildScope], such as
+ * [FrpBuildScope.toSharedFlow] or [FrpBuildScope.observe].
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.onEach(action: suspend FrpTransactionScope.(A) -> Unit): TFlow<A> = map {
+ action(it)
+ it
+}
+
+/**
+ * Returns a [TFlow] containing only values of the original [TFlow] that satisfy the given
+ * [predicate].
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.filter(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> {
+ val pulse =
+ filterNode({ init.connect(evalScope = this) }) { runInTransactionScope { predicate(it) } }
+ return TFlowInit(constInit(name = null, pulse.cached()))
+}
+
+/**
+ * Splits a [TFlow] of pairs into a pair of [TFlows][TFlow], where each returned [TFlow] emits half
+ * of the original.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * val lefts = map { it.first }
+ * val rights = map { it.second }
+ * return Pair(lefts, rights)
+ * ```
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<Pair<A, B>>.unzip(): Pair<TFlow<A>, TFlow<B>> {
+ val lefts = map { it.first }
+ val rights = map { it.second }
+ return lefts to rights
+}
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from both.
+ *
+ * Because [TFlow]s 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 [TFlow].
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.mergeWith(
+ other: TFlow<A>,
+ transformCoincidence: suspend FrpTransactionScope.(A, A) -> A = { a, _ -> a },
+): TFlow<A> {
+ val node =
+ mergeNodes(
+ getPulse = { init.connect(evalScope = this) },
+ getOther = { other.init.connect(evalScope = this) },
+ ) { a, b ->
+ runInTransactionScope { transformCoincidence(a, b) }
+ }
+ return TFlowInit(constInit(name = null, node))
+}
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalFrpApi
+fun <A> merge(vararg flows: TFlow<A>): TFlow<List<A>> = flows.asIterable().merge()
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [TFlow] is emitted.
+ *
+ * @see merge
+ */
+@ExperimentalFrpApi
+fun <A> mergeLeft(vararg flows: TFlow<A>): TFlow<A> = flows.asIterable().mergeLeft()
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all.
+ *
+ * Because [TFlow]s 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 [TFlow].
+ */
+// TODO: can be optimized to avoid creating the intermediate list
+fun <A> merge(vararg flows: TFlow<A>, transformCoincidence: (A, A) -> A): TFlow<A> =
+ merge(*flows).map { l -> l.reduce(transformCoincidence) }
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalFrpApi
+fun <A> Iterable<TFlow<A>>.merge(): TFlow<List<A>> =
+ TFlowInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [TFlow] is emitted.
+ *
+ * @see merge
+ */
+@ExperimentalFrpApi
+fun <A> Iterable<TFlow<A>>.mergeLeft(): TFlow<A> =
+ TFlowInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are
+ * collected into the emitted [List], preserving the input ordering.
+ *
+ * @see mergeWith
+ */
+@ExperimentalFrpApi fun <A> Sequence<TFlow<A>>.merge(): TFlow<List<A>> = asIterable().merge()
+
+/**
+ * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are
+ * collected into the emitted [Map], and are given the same key of the associated [TFlow] in the
+ * input [Map].
+ *
+ * @see mergeWith
+ */
+@ExperimentalFrpApi
+fun <K, A> Map<K, TFlow<A>>.merge(): TFlow<Map<K, A>> =
+ asSequence().map { (k, flowA) -> flowA.map { a -> k to a } }.toList().merge().map { it.toMap() }
+
+/**
+ * Returns a [GroupedTFlow] that can be used to efficiently split a single [TFlow] into multiple
+ * downstream [TFlow]s.
+ *
+ * The input [TFlow] emits [Map] instances that specify which downstream [TFlow] the associated
+ * value will be emitted from. These downstream [TFlow]s can be obtained via
+ * [GroupedTFlow.eventsForKey].
+ *
+ * An example:
+ * ```
+ * val sFoo: TFlow<Map<String, Foo>> = ...
+ * val fooById: GroupedTFlow<String, Foo> = sFoo.groupByKey()
+ * val fooBar: TFlow<Foo> = fooById["bar"]
+ * ```
+ *
+ * This is semantically equivalent to `val fooBar = sFoo.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 [TFlow], and so operates in `O(1)`.
+ *
+ * Note that the result [GroupedTFlow] should be cached and re-used to gain the performance benefit.
+ *
+ * @see selector
+ */
+@ExperimentalFrpApi
+fun <K, A> TFlow<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedTFlow<K, A> =
+ GroupedTFlow(DemuxImpl({ init.connect(this) }, numKeys))
+
+/**
+ * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
+ *
+ * @see groupByKey
+ */
+@ExperimentalFrpApi
+fun <K, A> TFlow<A>.groupBy(
+ numKeys: Int? = null,
+ extractKey: suspend FrpTransactionScope.(A) -> K,
+): GroupedTFlow<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+
+/**
+ * Returns two new [TFlow]s that contain elements from this [TFlow] 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.
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.partition(
+ predicate: suspend FrpTransactionScope.(A) -> Boolean
+): Pair<TFlow<A>, TFlow<A>> {
+ val grouped: GroupedTFlow<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
+ return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
+}
+
+/**
+ * Returns two new [TFlow]s that contain elements from this [TFlow]; [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.
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<Either<A, B>>.partitionEither(): Pair<TFlow<A>, TFlow<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 [TFlow]s emitting values of type [A].
+ *
+ * @see groupByKey
+ */
+@ExperimentalFrpApi
+class GroupedTFlow<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
+ /**
+ * Returns a [TFlow] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ @ExperimentalFrpApi
+ fun eventsForKey(key: K): TFlow<A> = TFlowInit(constInit(name = null, impl.eventsForKey(key)))
+
+ /**
+ * Returns a [TFlow] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ @ExperimentalFrpApi operator fun get(key: K): TFlow<A> = eventsForKey(key)
+}
+
+/**
+ * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it
+ * changes.
+ *
+ * This switch does take effect until the *next* transaction after [TState] changes. For a switch
+ * that takes effect immediately, see [switchPromptly].
+ */
+@ExperimentalFrpApi
+fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
+ return TFlowInit(
+ constInit(
+ name = null,
+ switchDeferredImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = {
+ mapImpl({ init.connect(this).changes }) { newFlow ->
+ newFlow.init.connect(this)
+ }
+ },
+ ),
+ )
+ )
+}
+
+/**
+ * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it
+ * changes.
+ *
+ * This switch takes effect immediately within the same transaction that [TState] changes. In
+ * general, you should prefer [switch] over this method. It is both safer and more performant.
+ */
+// TODO: parameter to handle coincidental emission from both old and new
+@ExperimentalFrpApi
+fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
+ val switchNode =
+ switchPromptImpl(
+ getStorage = {
+ mapOf(Unit to init.connect(this).getCurrentWithEpoch(this).first.init.connect(this))
+ },
+ getPatches = {
+ val patches = init.connect(this).changes
+ mapImpl({ patches }) { newFlow -> mapOf(Unit to just(newFlow.init.connect(this))) }
+ },
+ )
+ return TFlowInit(constInit(name = null, mapImpl({ switchNode }) { it.getValue(Unit) }))
+}
+
+/**
+ * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure
+ * by coalescing all emissions into batches.
+ *
+ * @see FrpNetwork.coalescingMutableTFlow
+ */
+@ExperimentalFrpApi
+class CoalescingMutableTFlow<In, Out>
+internal constructor(
+ internal val coalesce: (old: Out, new: In) -> Out,
+ internal val network: Network,
+ private val getInitialValue: () -> Out,
+ internal val impl: InputNode<Out> = InputNode(),
+) : TFlow<Out>() {
+ internal val name: String? = null
+ internal val storage = AtomicReference(false to getInitialValue())
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+
+ /**
+ * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not
+ * already pending.
+ *
+ * Backpressure occurs when [emit] is called while the FRP network is currently in a
+ * transaction; if called multiple times, then emissions will be coalesced into a single batch
+ * that is then processed when the network is ready.
+ */
+ @ExperimentalFrpApi
+ fun emit(value: In) {
+ val (scheduled, _) = storage.getAndUpdate { (_, old) -> true to coalesce(old, value) }
+ if (!scheduled) {
+ @Suppress("DeferredResultUnused")
+ network.transaction {
+ impl.visit(this, storage.getAndSet(false to getInitialValue()).second)
+ }
+ }
+ }
+}
+
+/**
+ * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure
+ * by suspending the emitter.
+ *
+ * @see FrpNetwork.coalescingMutableTFlow
+ */
+@ExperimentalFrpApi
+class MutableTFlow<T>
+internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) :
+ TFlow<T>() {
+ internal val name: String? = null
+
+ private val storage = AtomicReference<Job?>(null)
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+
+ /**
+ * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing
+ * the emission has completed.
+ */
+ @ExperimentalFrpApi
+ suspend fun emit(value: T) {
+ coroutineScope {
+ val newEmit =
+ async(start = CoroutineStart.LAZY) {
+ network.transaction { impl.visit(this, value) }.await()
+ }
+ val jobOrNull = storage.getAndSet(newEmit)
+ if (jobOrNull?.isActive != true) {
+ newEmit.await()
+ } else {
+ jobOrNull.join()
+ }
+ }
+ }
+
+ // internal suspend fun emitInCurrentTransaction(value: T, evalScope: EvalScope) {
+ // if (storage.getAndSet(just(value)) is None) {
+ // impl.visit(evalScope)
+ // }
+ // }
+}
+
+private data object EmptyFlow : TFlow<Nothing>()
+
+internal class TFlowInit<out A>(val init: Init<TFlowImpl<A>>) : TFlow<A>() {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal val <A> TFlow<A>.init: Init<TFlowImpl<A>>
+ get() =
+ when (this) {
+ is EmptyFlow -> constInit("EmptyFlow", neverImpl)
+ is TFlowInit -> init
+ is TFlowLoop -> init
+ is CoalescingMutableTFlow<*, A> -> constInit(name, impl.activated())
+ is MutableTFlow -> constInit(name, impl.activated())
+ }
+
+private inline fun <A> deferInline(crossinline block: suspend InitScope.() -> TFlow<A>): TFlow<A> =
+ TFlowInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt
new file mode 100644
index 0000000..a4c6956
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt
@@ -0,0 +1,492 @@
+/*
+ * 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.internal.DerivedMapCheap
+import com.android.systemui.kairos.internal.Init
+import com.android.systemui.kairos.internal.InitScope
+import com.android.systemui.kairos.internal.Network
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.Schedulable
+import com.android.systemui.kairos.internal.TFlowImpl
+import com.android.systemui.kairos.internal.TStateImpl
+import com.android.systemui.kairos.internal.TStateSource
+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.constS
+import com.android.systemui.kairos.internal.filterNode
+import com.android.systemui.kairos.internal.flatMap
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.map
+import com.android.systemui.kairos.internal.mapCheap
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.internal.zipStates
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
+ * holds a value, and a [TFlow] that emits when the value changes.
+ */
+@ExperimentalFrpApi sealed class TState<out A>
+
+/** A [TState] that never changes. */
+@ExperimentalFrpApi
+fun <A> tStateOf(value: A): TState<A> {
+ val operatorName = "tStateOf"
+ val name = "$operatorName($value)"
+ return TStateInit(constInit(name, constS(name, operatorName, value)))
+}
+
+/** TODO */
+@ExperimentalFrpApi fun <A> Lazy<TState<A>>.defer(): TState<A> = deferInline { value }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> FrpDeferredValue<TState<A>>.defer(): TState<A> = deferInline { unwrapped.await() }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> deferTState(block: suspend FrpScope.() -> TState<A>): TState<A> = deferInline {
+ NoScope.runInFrpScope(block)
+}
+
+/**
+ * Returns a [TState] containing the results of applying [transform] to the value held by the
+ * original [TState].
+ */
+@ExperimentalFrpApi
+fun <A, B> TState<A>.map(transform: suspend FrpScope.(A) -> B): TState<B> {
+ val operatorName = "map"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ init.connect(evalScope = this).map(name, operatorName) {
+ NoScope.runInFrpScope { transform(it) }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] that transforms the value held inside this [TState] 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 [stateChanges]
+ * for the returned [TState] will operate unexpectedly, emitting at rates that do not reflect an
+ * observable change to the returned [TState].
+ */
+@ExperimentalFrpApi
+fun <A, B> TState<A>.mapCheapUnsafe(transform: suspend FrpScope.(A) -> B): TState<B> {
+ val operatorName = "map"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ init.connect(evalScope = this).mapCheap(name, operatorName) {
+ NoScope.runInFrpScope { transform(it) }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] by combining the values held inside the given [TState]s by applying them to
+ * the given function [transform].
+ */
+@ExperimentalFrpApi
+fun <A, B, C> TState<A>.combineWith(
+ other: TState<B>,
+ transform: suspend FrpScope.(A, B) -> C,
+): TState<C> = combine(this, other, transform)
+
+/**
+ * Splits a [TState] of pairs into a pair of [TFlows][TState], where each returned [TState] holds
+ * hald of the original.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * val lefts = map { it.first }
+ * val rights = map { it.second }
+ * return Pair(lefts, rights)
+ * ```
+ */
+@ExperimentalFrpApi
+fun <A, B> TState<Pair<A, B>>.unzip(): Pair<TState<A>, TState<B>> {
+ val left = map { it.first }
+ val right = map { it.second }
+ return left to right
+}
+
+/**
+ * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [List].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A> Iterable<TState<A>>.combine(): TState<List<A>> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) })
+ }
+ )
+}
+
+/**
+ * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [Map].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <K : Any, A> Map<K, TState<A>>.combine(): TState<Map<K, A>> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ zipStates(
+ name,
+ operatorName,
+ states = mapValues { it.value.init.connect(evalScope = this) },
+ )
+ }
+ )
+}
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B> Iterable<TState<A>>.combine(transform: suspend FrpScope.(List<A>) -> B): TState<B> =
+ combine().map(transform)
+
+/**
+ * Returns a [TState] by combining the values held inside the given [TState]s into a [List].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A> combine(vararg states: TState<A>): TState<List<A>> = states.asIterable().combine()
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B> combine(
+ vararg states: TState<A>,
+ transform: suspend FrpScope.(List<A>) -> B,
+): TState<B> = states.asIterable().combine(transform)
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ transform: suspend FrpScope.(A, B) -> Z,
+): TState<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ coroutineScope {
+ val dl1: Deferred<TStateImpl<A>> = async {
+ stateA.init.connect(evalScope = this@init)
+ }
+ val dl2: Deferred<TStateImpl<B>> = async {
+ stateB.init.connect(evalScope = this@init)
+ }
+ zipStates(name, operatorName, dl1.await(), dl2.await()) { a, b ->
+ NoScope.runInFrpScope { transform(a, b) }
+ }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B, C, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ stateC: TState<C>,
+ transform: suspend FrpScope.(A, B, C) -> Z,
+): TState<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ coroutineScope {
+ val dl1: Deferred<TStateImpl<A>> = async {
+ stateA.init.connect(evalScope = this@init)
+ }
+ val dl2: Deferred<TStateImpl<B>> = async {
+ stateB.init.connect(evalScope = this@init)
+ }
+ val dl3: Deferred<TStateImpl<C>> = async {
+ stateC.init.connect(evalScope = this@init)
+ }
+ zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await()) { a, b, c ->
+ NoScope.runInFrpScope { transform(a, b, c) }
+ }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B, C, D, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ stateC: TState<C>,
+ stateD: TState<D>,
+ transform: suspend FrpScope.(A, B, C, D) -> Z,
+): TState<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ coroutineScope {
+ val dl1: Deferred<TStateImpl<A>> = async {
+ stateA.init.connect(evalScope = this@init)
+ }
+ val dl2: Deferred<TStateImpl<B>> = async {
+ stateB.init.connect(evalScope = this@init)
+ }
+ val dl3: Deferred<TStateImpl<C>> = async {
+ stateC.init.connect(evalScope = this@init)
+ }
+ val dl4: Deferred<TStateImpl<D>> = async {
+ stateD.init.connect(evalScope = this@init)
+ }
+ zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await(), dl4.await()) {
+ a,
+ b,
+ c,
+ d ->
+ NoScope.runInFrpScope { transform(a, b, c, d) }
+ }
+ }
+ }
+ )
+}
+
+/** Returns a [TState] by applying [transform] to the value held by the original [TState]. */
+@ExperimentalFrpApi
+fun <A, B> TState<A>.flatMap(transform: suspend FrpScope.(A) -> TState<B>): TState<B> {
+ val operatorName = "flatMap"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ init.connect(this).flatMap(name, operatorName) { a ->
+ NoScope.runInFrpScope { transform(a) }.init.connect(this)
+ }
+ }
+ )
+}
+
+/** Shorthand for `flatMap { it }` */
+@ExperimentalFrpApi fun <A> TState<TState<A>>.flatten() = flatMap { it }
+
+/**
+ * Returns a [TStateSelector] that can be used to efficiently check if the input [TState] is
+ * currently holding a specific value.
+ *
+ * An example:
+ * ```
+ * val lInt: TState<Int> = ...
+ * val intSelector: TStateSelector<Int> = lInt.selector()
+ * // Tracks if lInt is holding 1
+ * val isOne: TState<Boolean> = intSelector.whenSelected(1)
+ * ```
+ *
+ * This is semantically equivalent to `val isOne = lInt.map { i -> i == 1 }`, but is significantly
+ * more efficient; specifically, using [TState.map] in this way incurs a `O(n)` performance hit,
+ * where `n` is the number of different [TState.map] operations used to track a specific value.
+ * [selector] internally uses a [HashMap] to lookup the appropriate downstream [TState] to update,
+ * and so operates in `O(1)`.
+ *
+ * Note that the result [TStateSelector] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @see groupByKey
+ */
+@ExperimentalFrpApi
+fun <A> TState<A>.selector(numDistinctValues: Int? = null): TStateSelector<A> =
+ TStateSelector(
+ this,
+ stateChanges
+ .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
+ .groupByKey(numDistinctValues),
+ )
+
+/**
+ * Tracks the currently selected value of type [A] from an upstream [TState].
+ *
+ * @see selector
+ */
+@ExperimentalFrpApi
+class TStateSelector<A>
+internal constructor(
+ private val upstream: TState<A>,
+ private val groupedChanges: GroupedTFlow<A, Boolean>,
+) {
+ /**
+ * Returns a [TState] that tracks whether the upstream [TState] is currently holding the given
+ * [value].
+ *
+ * @see selector
+ */
+ @ExperimentalFrpApi
+ fun whenSelected(value: A): TState<Boolean> {
+ val operatorName = "TStateSelector#whenSelected"
+ val name = "$operatorName[$value]"
+ return TStateInit(
+ init(name) {
+ DerivedMapCheap(
+ name,
+ operatorName,
+ upstream = upstream.init.connect(evalScope = this),
+ changes = groupedChanges.impl.eventsForKey(value),
+ ) {
+ it == value
+ }
+ }
+ )
+ }
+
+ @ExperimentalFrpApi operator fun get(value: A): TState<Boolean> = whenSelected(value)
+}
+
+/** TODO */
+@ExperimentalFrpApi
+class MutableTState<T>
+internal constructor(internal val network: Network, initialValue: Deferred<T>) : TState<T>() {
+
+ private val input: CoalescingMutableTFlow<Deferred<T>, Deferred<T>?> =
+ CoalescingMutableTFlow(
+ coalesce = { _, new -> new },
+ network = network,
+ getInitialValue = { null },
+ )
+
+ internal val tState = run {
+ val changes = input.impl
+ val name = null
+ val operatorName = "MutableTState"
+ lateinit var state: TStateSource<T>
+ val calm: TFlowImpl<T> =
+ filterNode({ mapImpl(upstream = { changes.activated() }) { it!!.await() } }) { new ->
+ new != state.getCurrentWithEpoch(evalScope = this).first
+ }
+ .cached()
+ state = TStateSource(name, operatorName, initialValue, calm)
+ @Suppress("DeferredResultUnused")
+ network.transaction {
+ calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let {
+ (connection, needsEval) ->
+ state.upstreamConnection = connection
+ if (needsEval) {
+ schedule(state)
+ }
+ }
+ }
+ TStateInit(constInit(name, state))
+ }
+
+ /** TODO */
+ @ExperimentalFrpApi fun setValue(value: T) = input.emit(CompletableDeferred(value))
+
+ @ExperimentalFrpApi
+ fun setValueDeferred(value: FrpDeferredValue<T>) = input.emit(value.unwrapped)
+}
+
+/** A forward-reference to a [TState], allowing for recursive definitions. */
+@ExperimentalFrpApi
+class TStateLoop<A> : TState<A>() {
+
+ private val name: String? = null
+
+ private val deferred = CompletableDeferred<TState<A>>()
+
+ internal val init: Init<TStateImpl<A>> =
+ init(name) { deferred.await().init.connect(evalScope = this) }
+
+ /** The [TState] this [TStateLoop] will forward to. */
+ @ExperimentalFrpApi
+ var loopback: TState<A>? = null
+ set(value) {
+ value?.let {
+ check(deferred.complete(value)) { "TStateLoop.loopback has already been set." }
+ field = value
+ }
+ }
+
+ @ExperimentalFrpApi
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): TState<A> = this
+
+ @ExperimentalFrpApi
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TState<A>) {
+ loopback = value
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal class TStateInit<A> internal constructor(internal val init: Init<TStateImpl<A>>) :
+ TState<A>() {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal val <A> TState<A>.init: Init<TStateImpl<A>>
+ get() =
+ when (this) {
+ is TStateInit -> init
+ is TStateLoop -> init
+ is MutableTState -> tState.init
+ }
+
+private inline fun <A> deferInline(
+ crossinline block: suspend InitScope.() -> TState<A>
+): TState<A> = TStateInit(init(name = null) { block().init.connect(evalScope = this) })
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
new file mode 100644
index 0000000..6b1c8c8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.internal.InitScope
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.TransactionalImpl
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.transactionalImpl
+import com.android.systemui.kairos.internal.util.hashString
+import kotlinx.coroutines.CompletableDeferred
+
+/**
+ * A time-varying value. A [Transactional] encapsulates the idea of some continuous state; each time
+ * it is "sampled", a new result may be produced.
+ *
+ * Because FRP 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_.
+ */
+@ExperimentalFrpApi
+class Transactional<out A> internal constructor(internal val impl: TState<TransactionalImpl<A>>) {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+/** A constant [Transactional] that produces [value] whenever it is sampled. */
+@ExperimentalFrpApi
+fun <A> transactionalOf(value: A): Transactional<A> =
+ Transactional(tStateOf(TransactionalImpl.Const(CompletableDeferred(value))))
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline {
+ unwrapped.await()
+}
+
+/** TODO */
+@ExperimentalFrpApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> deferTransactional(block: suspend FrpScope.() -> Transactional<A>): Transactional<A> =
+ deferInline {
+ NoScope.runInFrpScope(block)
+ }
+
+private inline fun <A> deferInline(
+ crossinline block: suspend InitScope.() -> Transactional<A>
+): Transactional<A> =
+ Transactional(TStateInit(init(name = null) { block().impl.init.connect(evalScope = this) }))
+
+/**
+ * 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.
+ */
+@ExperimentalFrpApi
+fun <A> transactionally(block: suspend FrpTransactionScope.() -> A): Transactional<A> =
+ Transactional(tStateOf(transactionalImpl { runInTransactionScope(block) }))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt
new file mode 100644
index 0000000..4f302a1
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.debug
+
+import com.android.systemui.kairos.MutableTState
+import com.android.systemui.kairos.TState
+import com.android.systemui.kairos.TStateInit
+import com.android.systemui.kairos.TStateLoop
+import com.android.systemui.kairos.internal.DerivedFlatten
+import com.android.systemui.kairos.internal.DerivedMap
+import com.android.systemui.kairos.internal.DerivedMapCheap
+import com.android.systemui.kairos.internal.DerivedZipped
+import com.android.systemui.kairos.internal.Init
+import com.android.systemui.kairos.internal.TStateDerived
+import com.android.systemui.kairos.internal.TStateImpl
+import com.android.systemui.kairos.internal.TStateSource
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.None
+import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.orElseGet
+
+// object IdGen {
+// private val counter = AtomicLong()
+// fun getId() = counter.getAndIncrement()
+// }
+
+typealias StateGraph = Graph<ActivationInfo>
+
+sealed class StateInfo(
+ val name: String,
+ val value: Maybe<Any?>,
+ val operator: String,
+ val epoch: Long?,
+)
+
+class Source(name: String, value: Maybe<Any?>, operator: String, epoch: Long) :
+ StateInfo(name, value, operator, epoch)
+
+class Derived(
+ name: String,
+ val type: DerivedStateType,
+ value: Maybe<Any?>,
+ operator: String,
+ epoch: Long?,
+) : StateInfo(name, value, operator, epoch)
+
+sealed interface DerivedStateType
+
+data object Flatten : DerivedStateType
+
+data class Mapped(val cheap: Boolean) : DerivedStateType
+
+data object Combine : DerivedStateType
+
+sealed class InitInfo(val name: String)
+
+class Uninitialized(name: String) : InitInfo(name)
+
+class Initialized(val state: StateInfo) : InitInfo(state.name)
+
+sealed interface ActivationInfo
+
+class Inactive(val name: String) : ActivationInfo
+
+class Active(val nodeInfo: StateInfo) : ActivationInfo
+
+class Dead(val name: String) : ActivationInfo
+
+data class Edge(val upstream: Any, val downstream: Any, val tag: Any? = null)
+
+data class Graph<T>(val nodes: Map<Any, T>, val edges: List<Edge>)
+
+internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
+ val init: Init<TStateImpl<Any?>> =
+ when (this) {
+ is TStateInit -> init
+ is TStateLoop -> init
+ is MutableTState -> tState.init
+ }
+ when (val stateMaybe = init.getUnsafe()) {
+ None -> {
+ infoMap[this] = Uninitialized(init.name ?: init.toString())
+ }
+ is Just -> {
+ stateMaybe.value.dump(infoMap, edges)
+ }
+ }
+}
+
+internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
+ val state = this
+ if (state in infoById) return
+ val stateInfo =
+ when (state) {
+ is TStateDerived -> {
+ val type =
+ when (state) {
+ is DerivedFlatten -> {
+ state.upstream.dump(infoById, edges)
+ edges.add(
+ Edge(upstream = state.upstream, downstream = state, tag = "outer")
+ )
+ state.upstream
+ .getUnsafe()
+ .orElseGet { null }
+ ?.let {
+ edges.add(
+ Edge(upstream = it, downstream = state, tag = "inner")
+ )
+ it.dump(infoById, edges)
+ }
+ Flatten
+ }
+ is DerivedMap<*, *> -> {
+ state.upstream.dump(infoById, edges)
+ edges.add(Edge(upstream = state.upstream, downstream = state))
+ Mapped(cheap = false)
+ }
+ is DerivedZipped<*, *> -> {
+ state.upstream.forEach { (key, upstream) ->
+ edges.add(
+ Edge(upstream = upstream, downstream = state, tag = "key=$key")
+ )
+ upstream.dump(infoById, edges)
+ }
+ Combine
+ }
+ }
+ Derived(
+ state.name ?: state.operatorName,
+ type,
+ state.getCachedUnsafe(),
+ state.operatorName,
+ state.invalidatedEpoch,
+ )
+ }
+ is TStateSource ->
+ Source(
+ state.name ?: state.operatorName,
+ state.getStorageUnsafe(),
+ state.operatorName,
+ state.writeEpoch,
+ )
+ is DerivedMapCheap<*, *> -> {
+ state.upstream.dump(infoById, edges)
+ edges.add(Edge(upstream = state.upstream, downstream = state))
+ val type = Mapped(cheap = true)
+ Derived(
+ state.name ?: state.operatorName,
+ type,
+ state.getUnsafe(),
+ state.operatorName,
+ null,
+ )
+ }
+ }
+ infoById[state] = Initialized(stateInfo)
+}
+
+private fun <A> TStateImpl<A>.getUnsafe(): Maybe<A> =
+ when (this) {
+ is TStateDerived -> getCachedUnsafe()
+ is TStateSource -> getStorageUnsafe()
+ is DerivedMapCheap<*, *> -> none
+ }
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
new file mode 100644
index 0000000..90f1aea
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
@@ -0,0 +1,363 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.CoalescingMutableTFlow
+import com.android.systemui.kairos.FrpBuildScope
+import com.android.systemui.kairos.FrpCoalescingProducerScope
+import com.android.systemui.kairos.FrpDeferredValue
+import com.android.systemui.kairos.FrpEffectScope
+import com.android.systemui.kairos.FrpNetwork
+import com.android.systemui.kairos.FrpProducerScope
+import com.android.systemui.kairos.FrpSpec
+import com.android.systemui.kairos.FrpStateScope
+import com.android.systemui.kairos.FrpTransactionScope
+import com.android.systemui.kairos.GroupedTFlow
+import com.android.systemui.kairos.LocalFrpNetwork
+import com.android.systemui.kairos.MutableTFlow
+import com.android.systemui.kairos.TFlow
+import com.android.systemui.kairos.TFlowInit
+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.launchOnCancel
+import com.android.systemui.kairos.internal.util.mapValuesParallel
+import com.android.systemui.kairos.launchEffect
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.None
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.map
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CompletableJob
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope: CoroutineScope) :
+ BuildScope, StateScope by stateScope {
+
+ private val job: Job
+ get() = coroutineScope.coroutineContext.job
+
+ override val frpScope: FrpBuildScope = FrpBuildScopeImpl()
+
+ override suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R {
+ val complete = CompletableDeferred<R>(parent = coroutineContext.job)
+ block.startCoroutine(
+ frpScope,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+
+ private fun <A, T : TFlow<A>, S> buildTFlow(
+ constructFlow: (InputNode<A>) -> Pair<T, S>,
+ builder: suspend S.() -> Unit,
+ ): TFlow<A> {
+ var job: Job? = null
+ val stopEmitter = newStopEmitter()
+ val handle = this.job.invokeOnCompletion { stopEmitter.emit(Unit) }
+ // Create a child scope that will be kept alive beyond the end of this transaction.
+ val childScope = coroutineScope.childScope()
+ lateinit var emitter: Pair<T, S>
+ val inputNode =
+ InputNode<A>(
+ activate = {
+ check(job == null) { "already activated" }
+ job =
+ reenterBuildScope(this@BuildScopeImpl, childScope).runInBuildScope {
+ launchEffect {
+ builder(emitter.second)
+ handle.dispose()
+ stopEmitter.emit(Unit)
+ }
+ }
+ },
+ deactivate = {
+ checkNotNull(job) { "already deactivated" }.cancel()
+ job = null
+ },
+ )
+ emitter = constructFlow(inputNode)
+ return with(frpScope) { emitter.first.takeUntil(stopEmitter) }
+ }
+
+ private fun <T> tFlowInternal(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> =
+ buildTFlow(
+ constructFlow = { inputNode ->
+ val flow = MutableTFlow(network, inputNode)
+ flow to
+ object : FrpProducerScope<T> {
+ override suspend fun emit(value: T) {
+ flow.emit(value)
+ }
+ }
+ },
+ builder = builder,
+ )
+
+ private fun <In, Out> coalescingTFlowInternal(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+ ): TFlow<Out> =
+ buildTFlow(
+ constructFlow = { inputNode ->
+ val flow = CoalescingMutableTFlow(coalesce, network, getInitialValue, inputNode)
+ flow to
+ object : FrpCoalescingProducerScope<In> {
+ override fun emit(value: In) {
+ flow.emit(value)
+ }
+ }
+ },
+ builder = builder,
+ )
+
+ private fun <A> asyncScopeInternal(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> {
+ val childScope = mutableChildBuildScope()
+ return FrpDeferredValue(deferAsync { childScope.runInBuildScope(block) }) to childScope.job
+ }
+
+ private fun <R> deferredInternal(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> =
+ FrpDeferredValue(deferAsync { runInBuildScope(block) })
+
+ private fun deferredActionInternal(block: suspend FrpBuildScope.() -> Unit) {
+ deferAction { runInBuildScope(block) }
+ }
+
+ private fun <A> TFlow<A>.observeEffectInternal(
+ context: CoroutineContext,
+ block: suspend FrpEffectScope.(A) -> Unit,
+ ): Job {
+ val subRef = AtomicReference<Maybe<Output<A>>>(null)
+ val childScope = coroutineScope.childScope()
+ // When our scope is cancelled, deactivate this observer.
+ childScope.launchOnCancel(CoroutineName("TFlow.observeEffect")) {
+ subRef.getAndSet(None)?.let { output ->
+ if (output is Just) {
+ @Suppress("DeferredResultUnused")
+ network.transaction { scheduleDeactivation(output.value) }
+ }
+ }
+ }
+ // Defer so that we don't suspend the caller
+ deferAction {
+ val outputNode =
+ Output<A>(
+ context = context,
+ onDeath = { subRef.getAndSet(None)?.let { childScope.cancel() } },
+ onEmit = { output ->
+ if (subRef.get() is Just) {
+ // Not cancelled, safe to emit
+ val coroutine: suspend FrpEffectScope.() -> Unit = { block(output) }
+ val complete = CompletableDeferred<Unit>(parent = coroutineContext.job)
+ coroutine.startCoroutine(
+ object : FrpEffectScope, FrpTransactionScope by frpScope {
+ override val frpCoroutineScope: CoroutineScope = childScope
+ override val frpNetwork: FrpNetwork =
+ LocalFrpNetwork(network, childScope, endSignal)
+ },
+ completion =
+ object : Continuation<Unit> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<Unit>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ complete.await()
+ }
+ },
+ )
+ with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) }
+ .init
+ .connect(evalScope = stateScope.evalScope)
+ .activate(evalScope = stateScope.evalScope, outputNode.schedulable)
+ ?.let { (conn, needsEval) ->
+ outputNode.upstream = conn
+ if (!subRef.compareAndSet(null, just(outputNode))) {
+ // Job's already been cancelled, schedule deactivation
+ scheduleDeactivation(outputNode)
+ } else if (needsEval) {
+ outputNode.schedule(evalScope = stateScope.evalScope)
+ }
+ } ?: childScope.cancel()
+ }
+ return childScope.coroutineContext.job
+ }
+
+ private fun <A, B> TFlow<A>.mapBuildInternal(
+ transform: suspend FrpBuildScope.(A) -> B
+ ): TFlow<B> {
+ val childScope = coroutineScope.childScope()
+ return TFlowInit(
+ constInit(
+ "mapBuild",
+ mapImpl({ init.connect(evalScope = this) }) { spec ->
+ reenterBuildScope(outerScope = this@BuildScopeImpl, childScope)
+ .runInBuildScope {
+ val (result, _) = asyncScope { transform(spec) }
+ result.get()
+ }
+ }
+ .cached(),
+ )
+ )
+ }
+
+ private fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestForKeyInternal(
+ init: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> {
+ val eventsByKey: GroupedTFlow<K, Maybe<FrpSpec<A>>> = groupByKey(numKeys)
+ val initOut: Deferred<Map<K, B>> = deferAsync {
+ init.unwrapped.await().mapValuesParallel { (k, spec) ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childBuildScope(newEnd)
+ newScope.runInBuildScope(spec)
+ }
+ }
+ val childScope = coroutineScope.childScope()
+ val changesNode: TFlowImpl<Map<K, Maybe<A>>> =
+ mapImpl(upstream = { this@applyLatestForKeyInternal.init.connect(evalScope = this) }) {
+ upstreamMap ->
+ reenterBuildScope(this@BuildScopeImpl, childScope).run {
+ upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpSpec<A>>) ->
+ ma.map { spec ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childBuildScope(newEnd)
+ newScope.runInBuildScope(spec)
+ }
+ }
+ }
+ }
+ val changes: TFlow<Map<K, Maybe<A>>> =
+ TFlowInit(constInit("applyLatestForKey", changesNode.cached()))
+ // Ensure effects are observed; otherwise init will stay alive longer than expected
+ changes.observeEffectInternal(EmptyCoroutineContext) {}
+ return changes to FrpDeferredValue(initOut)
+ }
+
+ private fun newStopEmitter(): CoalescingMutableTFlow<Unit, Unit> =
+ CoalescingMutableTFlow(
+ coalesce = { _, _: Unit -> },
+ network = network,
+ getInitialValue = {},
+ )
+
+ private suspend fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl {
+ val newCoroutineScope: CoroutineScope = coroutineScope.childScope()
+ return BuildScopeImpl(
+ stateScope = stateScope.childStateScope(newEnd),
+ coroutineScope = newCoroutineScope,
+ )
+ .apply {
+ // Ensure that once this transaction is done, the new child scope enters the
+ // completing state (kept alive so long as there are child jobs).
+ scheduleOutput(
+ OneShot {
+ // TODO: don't like this cast
+ (newCoroutineScope.coroutineContext.job as CompletableJob).complete()
+ }
+ )
+ runInBuildScope { endSignal.nextOnly().observe { newCoroutineScope.cancel() } }
+ }
+ }
+
+ private fun mutableChildBuildScope(): BuildScopeImpl {
+ val stopEmitter = newStopEmitter()
+ 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).
+ scheduleOutput(
+ OneShot {
+ // TODO: don't like this cast
+ (childScope.coroutineContext.job as CompletableJob).complete()
+ }
+ )
+ return BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter),
+ coroutineScope = childScope,
+ )
+ }
+
+ private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope {
+
+ override fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> =
+ tFlowInternal(builder)
+
+ override fun <In, Out> coalescingTFlow(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+ ): TFlow<Out> = coalescingTFlowInternal(getInitialValue, coalesce, builder)
+
+ override fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> =
+ asyncScopeInternal(block)
+
+ override fun <R> deferredBuildScope(
+ block: suspend FrpBuildScope.() -> R
+ ): FrpDeferredValue<R> = deferredInternal(block)
+
+ override fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) =
+ deferredActionInternal(block)
+
+ override fun <A> TFlow<A>.observe(
+ coroutineContext: CoroutineContext,
+ block: suspend FrpEffectScope.(A) -> Unit,
+ ): Job = observeEffectInternal(coroutineContext, block)
+
+ override fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> =
+ mapBuildInternal(transform)
+
+ override fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestForKeyInternal(initialSpecs, numKeys)
+ }
+}
+
+private fun EvalScope.reenterBuildScope(
+ outerScope: BuildScopeImpl,
+ coroutineScope: CoroutineScope,
+) =
+ BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal),
+ coroutineScope,
+ )
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt
new file mode 100644
index 0000000..f65307c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.asyncImmediate
+import com.android.systemui.kairos.internal.util.launchImmediate
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
+
+internal typealias DeferScope = CoroutineScope
+
+internal inline fun DeferScope.deferAction(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ crossinline block: suspend () -> Unit,
+): Job {
+ check(isActive) { "Cannot perform deferral, scope already closed." }
+ return launchImmediate(start, CoroutineName("deferAction")) { block() }
+}
+
+internal inline fun <R> DeferScope.deferAsync(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ crossinline block: suspend () -> R,
+): Deferred<R> {
+ check(isActive) { "Cannot perform deferral, scope already closed." }
+ return asyncImmediate(start, CoroutineName("deferAsync")) { block() }
+}
+
+internal suspend inline fun <A> deferScope(noinline block: suspend DeferScope.() -> A): A =
+ coroutineScope(block)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt
new file mode 100644
index 0000000..e7b9952
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.flatMap
+import com.android.systemui.kairos.util.getMaybe
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+internal class DemuxNode<K, A>(
+ private val branchNodeByKey: ConcurrentHashMap<K, DemuxBranchNode<K, A>>,
+ val lifecycle: DemuxLifecycle<K, A>,
+ private val spec: DemuxActivator<K, A>,
+) : SchedulableNode {
+
+ val schedulable = Schedulable.N(this)
+
+ inline val mutex
+ get() = lifecycle.mutex
+
+ lateinit var upstreamConnection: NodeConnection<Map<K, A>>
+
+ fun getAndMaybeAddDownstream(key: K): DemuxBranchNode<K, A> =
+ branchNodeByKey.getOrPut(key) { DemuxBranchNode(key, this) }
+
+ override suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult = upstreamConnection.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ coroutineScope {
+ val outerScope = this
+ mutex.withLock {
+ coroutineScope {
+ for ((key, _) in upstreamResult.value) {
+ launch {
+ branchNodeByKey[key]?.let { branch ->
+ outerScope.launch { branch.schedule(evalScope) }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.adjustDirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ oldDepth,
+ newDepth,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.moveIndirectUpstreamToDirect(
+ coroutineScope = this,
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.adjustIndirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ oldDepth,
+ newDepth,
+ removals,
+ additions,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.moveDirectUpstreamToIndirect(
+ coroutineScope = this,
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ lifecycle.lifecycleState = DemuxLifecycleState.Dead
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.removeIndirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ depth,
+ indirectSet,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ coroutineScope {
+ mutex.withLock {
+ lifecycle.lifecycleState = DemuxLifecycleState.Dead
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.removeDirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ depth,
+ )
+ }
+ }
+ }
+ }
+
+ suspend fun removeDownstreamAndDeactivateIfNeeded(key: K) {
+ val deactivate =
+ mutex.withLock {
+ branchNodeByKey.remove(key)
+ branchNodeByKey.isEmpty()
+ }
+ if (deactivate) {
+ // No need for mutex here; no more concurrent changes to can occur during this phase
+ lifecycle.lifecycleState = DemuxLifecycleState.Inactive(spec)
+ upstreamConnection.removeDownstreamAndDeactivateIfNeeded(downstream = schedulable)
+ }
+ }
+}
+
+internal class DemuxBranchNode<K, A>(val key: K, private val demuxNode: DemuxNode<K, A>) :
+ PushNode<A> {
+
+ private val mutex = Mutex()
+
+ val downstreamSet = DownstreamSet()
+
+ override val depthTracker: DepthTracker
+ get() = demuxNode.upstreamConnection.depthTracker
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ demuxNode.upstreamConnection.hasCurrentValue(transactionStore)
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> =
+ demuxNode.upstreamConnection.getPushEvent(evalScope).flatMap { it.getMaybe(key) }
+
+ override suspend fun addDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.add(downstream) }
+ }
+
+ override suspend fun removeDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.remove(downstream) }
+ }
+
+ override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ val canDeactivate =
+ mutex.withLock {
+ downstreamSet.remove(downstream)
+ downstreamSet.isEmpty()
+ }
+ if (canDeactivate) {
+ demuxNode.removeDownstreamAndDeactivateIfNeeded(key)
+ }
+ }
+
+ override suspend fun deactivateIfNeeded() {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ demuxNode.removeDownstreamAndDeactivateIfNeeded(key)
+ }
+ }
+
+ override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+
+ suspend fun schedule(evalScope: EvalScope) {
+ if (!coroutineScope { mutex.withLock { scheduleAll(downstreamSet, evalScope) } }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+}
+
+internal fun <K, A> DemuxImpl(
+ upstream: suspend EvalScope.() -> TFlowImpl<Map<K, A>>,
+ numKeys: Int?,
+): DemuxImpl<K, A> =
+ DemuxImpl(
+ DemuxLifecycle(
+ object : DemuxActivator<K, A> {
+ override suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: DemuxLifecycle<K, A>,
+ ): Pair<DemuxNode<K, A>, Boolean>? {
+ val dmux = DemuxNode(ConcurrentHashMap(numKeys ?: 16), lifecycle, this)
+ return upstream
+ .invoke(evalScope)
+ .activate(evalScope, downstream = dmux.schedulable)
+ ?.let { (conn, needsEval) ->
+ dmux.apply { upstreamConnection = conn } to needsEval
+ }
+ }
+ }
+ )
+ )
+
+internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) {
+ fun eventsForKey(key: K): TFlowImpl<A> = TFlowCheap { downstream ->
+ dmux.activate(evalScope = this, key)?.let { (branchNode, needsEval) ->
+ branchNode.addDownstream(downstream)
+ val branchNeedsEval = needsEval && branchNode.getPushEvent(evalScope = this) is Just
+ ActivationResult(
+ connection = NodeConnection(branchNode, branchNode),
+ needsEval = branchNeedsEval,
+ )
+ }
+ }
+}
+
+internal class DemuxLifecycle<K, A>(@Volatile var lifecycleState: DemuxLifecycleState<K, A>) {
+ val mutex = Mutex()
+
+ override fun toString(): String = "TFlowDmuxState[$hashString][$lifecycleState][$mutex]"
+
+ suspend fun activate(evalScope: EvalScope, key: K): Pair<DemuxBranchNode<K, A>, Boolean>? =
+ coroutineScope {
+ mutex
+ .withLock {
+ when (val state = lifecycleState) {
+ is DemuxLifecycleState.Dead -> null
+ is DemuxLifecycleState.Active ->
+ state.node.getAndMaybeAddDownstream(key) to
+ async {
+ state.node.upstreamConnection.hasCurrentValue(
+ evalScope.transactionStore
+ )
+ }
+ is DemuxLifecycleState.Inactive -> {
+ state.spec
+ .activate(evalScope, this@DemuxLifecycle)
+ .also { result ->
+ lifecycleState =
+ if (result == null) {
+ DemuxLifecycleState.Dead
+ } else {
+ DemuxLifecycleState.Active(result.first)
+ }
+ }
+ ?.let { (node, needsEval) ->
+ node.getAndMaybeAddDownstream(key) to
+ CompletableDeferred(needsEval)
+ }
+ }
+ }
+ }
+ ?.let { (branch, result) -> branch to result.await() }
+ }
+}
+
+internal sealed interface DemuxLifecycleState<out K, out A> {
+ class Inactive<K, A>(val spec: DemuxActivator<K, A>) : DemuxLifecycleState<K, A> {
+ override fun toString(): String = "Inactive"
+ }
+
+ class Active<K, A>(val node: DemuxNode<K, A>) : DemuxLifecycleState<K, A> {
+ override fun toString(): String = "Active(node=$node)"
+ }
+
+ data object Dead : DemuxLifecycleState<Nothing, Nothing>
+}
+
+internal interface DemuxActivator<K, A> {
+ suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: DemuxLifecycle<K, A>,
+ ): Pair<DemuxNode<K, A>, Boolean>?
+}
+
+internal inline fun <K, A> DemuxLifecycle(onSubscribe: DemuxActivator<K, A>) =
+ DemuxLifecycle(DemuxLifecycleState.Inactive(onSubscribe))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt
new file mode 100644
index 0000000..815473f
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.FrpDeferredValue
+import com.android.systemui.kairos.FrpTransactionScope
+import com.android.systemui.kairos.TFlow
+import com.android.systemui.kairos.TFlowInit
+import com.android.systemui.kairos.TFlowLoop
+import com.android.systemui.kairos.TState
+import com.android.systemui.kairos.TStateInit
+import com.android.systemui.kairos.Transactional
+import com.android.systemui.kairos.emptyTFlow
+import com.android.systemui.kairos.init
+import com.android.systemui.kairos.mapCheap
+import com.android.systemui.kairos.switch
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) :
+ EvalScope, NetworkScope by networkScope, DeferScope by deferScope {
+
+ private suspend fun <A> Transactional<A>.sample(): A =
+ impl.sample().sample(this@EvalScopeImpl).await()
+
+ private suspend fun <A> TState<A>.sample(): A =
+ init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first
+
+ private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A>
+ get() = FrpDeferredValue(deferAsync { sample() })
+
+ private val <A> TState<A>.deferredValue: FrpDeferredValue<A>
+ get() = FrpDeferredValue(deferAsync { sample() })
+
+ private val nowInternal: TFlow<Unit> by lazy {
+ var result by TFlowLoop<Unit>()
+ result =
+ TStateInit(
+ constInit(
+ "now",
+ mkState(
+ "now",
+ "now",
+ this,
+ { result.mapCheap { emptyTFlow }.init.connect(evalScope = this) },
+ CompletableDeferred(
+ TFlowInit(
+ constInit(
+ "now",
+ TFlowCheap {
+ ActivationResult(
+ connection = NodeConnection(AlwaysNode, AlwaysNode),
+ needsEval = true,
+ )
+ },
+ )
+ )
+ ),
+ ),
+ )
+ )
+ .switch()
+ result
+ }
+
+ private fun <R> deferredInternal(
+ block: suspend FrpTransactionScope.() -> R
+ ): FrpDeferredValue<R> = FrpDeferredValue(deferAsync { runInTransactionScope(block) })
+
+ override suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R {
+ val complete = CompletableDeferred<R>(parent = coroutineContext.job)
+ block.startCoroutine(
+ frpScope,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+
+ override val frpScope: FrpTransactionScope = FrpTransactionScopeImpl()
+
+ inner class FrpTransactionScopeImpl : FrpTransactionScope {
+ override fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue
+
+ override fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue
+
+ override fun <R> deferredTransactionScope(
+ block: suspend FrpTransactionScope.() -> R
+ ): FrpDeferredValue<R> = deferredInternal(block)
+
+ override val now: TFlow<Unit>
+ get() = nowInternal
+ }
+}
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
new file mode 100644
index 0000000..bc06a36
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.none
+
+internal inline fun <A, B> mapMaybeNode(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline f: suspend EvalScope.(A) -> Maybe<B>,
+): TFlowImpl<B> {
+ return DemuxImpl(
+ {
+ mapImpl(getPulse) {
+ val maybeResult = f(it)
+ if (maybeResult is Just) {
+ mapOf(Unit to maybeResult.value)
+ } else {
+ emptyMap()
+ }
+ }
+ },
+ numKeys = 1,
+ )
+ .eventsForKey(Unit)
+}
+
+internal inline fun <A> filterNode(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline f: suspend EvalScope.(A) -> Boolean,
+): TFlowImpl<A> = mapMaybeNode(getPulse) { if (f(it)) just(it) else none }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt
new file mode 100644
index 0000000..3aec319
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt
@@ -0,0 +1,530 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.Bag
+import java.util.TreeMap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Tracks all upstream connections for Mux nodes.
+ *
+ * Connections come in two flavors:
+ * 1. **DIRECT** :: The upstream node may emit events that would cause the owner of this depth
+ * tracker to also emit.
+ * 2. **INDIRECT** :: The upstream node will not emit events, but may start doing so in a future
+ * transaction (at which point its depth will change to DIRECT).
+ *
+ * DIRECT connections are the standard, active connections that propagate events through the graph.
+ * They are used to calculate the evaluation depth of a node, so that it is only visited once it is
+ * certain that all DIRECT upstream connections have already been visited (or are not emitting in
+ * the current transaction).
+ *
+ * It is *invalid* for a node to be directly upstream of itself. Doing so is an error.
+ *
+ * INDIRECT connections identify nodes that are still "alive" (should not be garbage-collected) but
+ * are presently "dormant". This only occurs when a MuxDeferredNode has nothing switched-in, but is
+ * still connected to its "patches" upstream node, implying that something *may* be switched-in at a
+ * later time.
+ *
+ * It is *invalid* for a node to be indirectly upstream of itself. These connections are
+ * automatically filtered out.
+ *
+ * When there are no connections, either DIRECT or INDIRECT, a node *dies* and all incoming/outgoing
+ * connections are freed so that it can be garbage-collected.
+ *
+ * Note that there is an edge case where a MuxDeferredNode is connected to itself via its "patches"
+ * upstream node. In this case:
+ * 1. If the node has switched-in upstream nodes, then this is perfectly valid. Downstream nodes
+ * will see a direct connection to this MuxDeferredNode.
+ * 2. Otherwise, the node would normally be considered "dormant" and downstream nodes would see an
+ * indirect connection. However, because a node cannot be indirectly upstream of itself, then the
+ * MuxDeferredNode sees no connection via its patches upstream node, and so is considered "dead".
+ * Conceptually, this makes some sense: The only way for this recursive MuxDeferredNode to become
+ * non-dormant is to switch some upstream nodes back in, but since the patches node is itself,
+ * this will never happen.
+ *
+ * This behavior underpins the recursive definition of `nextOnly`.
+ */
+internal class DepthTracker {
+
+ @Volatile var snapshotIsDirect = true
+ @Volatile private var snapshotIsIndirectRoot = false
+
+ private inline val snapshotIsIndirect: Boolean
+ get() = !snapshotIsDirect
+
+ @Volatile var snapshotIndirectDepth: Int = 0
+ @Volatile var snapshotDirectDepth: Int = 0
+
+ private val _snapshotIndirectRoots = HashSet<MuxDeferredNode<*, *>>()
+ val snapshotIndirectRoots
+ get() = _snapshotIndirectRoots.toSet()
+
+ private val indirectAdditions = HashSet<MuxDeferredNode<*, *>>()
+ private val indirectRemovals = HashSet<MuxDeferredNode<*, *>>()
+ private val dirty_directUpstreamDepths = TreeMap<Int, Int>()
+ private val dirty_indirectUpstreamDepths = TreeMap<Int, Int>()
+ private val dirty_indirectUpstreamRoots = Bag<MuxDeferredNode<*, *>>()
+ @Volatile var dirty_directDepth = 0
+ @Volatile private var dirty_indirectDepth = 0
+ @Volatile private var dirty_depthIsDirect = true
+ @Volatile private var dirty_isIndirectRoot = false
+
+ suspend fun schedule(scheduler: Scheduler, node: MuxNode<*, *, *>) {
+ if (dirty_depthIsDirect) {
+ scheduler.schedule(dirty_directDepth, node)
+ } else {
+ scheduler.scheduleIndirect(dirty_indirectDepth, node)
+ }
+ }
+
+ // only used by MuxDeferred
+ // and only when there is a direct connection to the patch node
+ fun setIsIndirectRoot(isRoot: Boolean): Boolean {
+ if (isRoot != dirty_isIndirectRoot) {
+ dirty_isIndirectRoot = isRoot
+ return !dirty_depthIsDirect
+ }
+ return false
+ }
+
+ // adds an upstream connection, and recalcs depth
+ // returns true if depth has changed
+ fun addDirectUpstream(oldDepth: Int?, newDepth: Int): Boolean {
+ if (oldDepth != null) {
+ dirty_directUpstreamDepths.compute(oldDepth) { _, count ->
+ count?.minus(1)?.takeIf { it > 0 }
+ }
+ }
+ dirty_directUpstreamDepths.compute(newDepth) { _, current -> current?.plus(1) ?: 1 }
+ return recalcDepth()
+ }
+
+ private fun recalcDepth(): Boolean {
+ val newDepth =
+ dirty_directUpstreamDepths.lastEntry()?.let { (maxDepth, _) -> maxDepth + 1 } ?: 0
+
+ val isDirect = dirty_directUpstreamDepths.isNotEmpty()
+ val isDirectChanged = dirty_depthIsDirect != isDirect
+ dirty_depthIsDirect = isDirect
+
+ return (newDepth != dirty_directDepth).also { dirty_directDepth = newDepth } or
+ isDirectChanged
+ }
+
+ private fun recalcIndirDepth(): Boolean {
+ val newDepth =
+ dirty_indirectUpstreamDepths.lastEntry()?.let { (maxDepth, _) -> maxDepth + 1 } ?: 0
+ return (!dirty_depthIsDirect && !dirty_isIndirectRoot && newDepth != dirty_indirectDepth)
+ .also { dirty_indirectDepth = newDepth }
+ }
+
+ fun removeDirectUpstream(depth: Int): Boolean {
+ dirty_directUpstreamDepths.compute(depth) { _, count -> count?.minus(1)?.takeIf { it > 0 } }
+ return recalcDepth()
+ }
+
+ fun addIndirectUpstream(oldDepth: Int?, newDepth: Int): Boolean =
+ if (oldDepth == newDepth) {
+ false
+ } else {
+ if (oldDepth != null) {
+ dirty_indirectUpstreamDepths.compute(oldDepth) { _, current ->
+ current?.minus(1)?.takeIf { it > 0 }
+ }
+ }
+ dirty_indirectUpstreamDepths.compute(newDepth) { _, current -> current?.plus(1) ?: 1 }
+ recalcIndirDepth()
+ }
+
+ fun removeIndirectUpstream(depth: Int): Boolean {
+ dirty_indirectUpstreamDepths.compute(depth) { _, current ->
+ current?.minus(1)?.takeIf { it > 0 }
+ }
+ return recalcIndirDepth()
+ }
+
+ fun updateIndirectRoots(
+ additions: Set<MuxDeferredNode<*, *>>? = null,
+ removals: Set<MuxDeferredNode<*, *>>? = null,
+ butNot: MuxDeferredNode<*, *>? = null,
+ ): Boolean {
+ val addsChanged =
+ additions
+ ?.let { dirty_indirectUpstreamRoots.addAll(additions, butNot) }
+ ?.let {
+ indirectAdditions.addAll(indirectRemovals.applyRemovalDiff(it))
+ true
+ } ?: false
+ val removalsChanged =
+ removals
+ ?.let { dirty_indirectUpstreamRoots.removeAll(removals) }
+ ?.let {
+ indirectRemovals.addAll(indirectAdditions.applyRemovalDiff(it))
+ true
+ } ?: false
+ return (!dirty_depthIsDirect && (addsChanged || removalsChanged))
+ }
+
+ private fun <T> HashSet<T>.applyRemovalDiff(changeSet: Set<T>): Set<T> {
+ val remainder = HashSet<T>()
+ for (element in changeSet) {
+ if (!add(element)) {
+ remainder.add(element)
+ }
+ }
+ return remainder
+ }
+
+ suspend fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *>) {
+ if (isDirty()) {
+ schedule(scheduler, muxNode)
+ }
+ }
+
+ fun applyChanges(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ downstreamSet: DownstreamSet,
+ muxNode: MuxNode<*, *, *>,
+ ) {
+ when {
+ dirty_depthIsDirect -> {
+ if (snapshotIsDirect) {
+ downstreamSet.adjustDirectUpstream(
+ coroutineScope,
+ scheduler,
+ oldDepth = snapshotDirectDepth,
+ newDepth = dirty_directDepth,
+ )
+ } else {
+ downstreamSet.moveIndirectUpstreamToDirect(
+ coroutineScope,
+ scheduler,
+ oldIndirectDepth = snapshotIndirectDepth,
+ oldIndirectSet =
+ buildSet {
+ addAll(snapshotIndirectRoots)
+ if (snapshotIsIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ newDirectDepth = dirty_directDepth,
+ )
+ }
+ }
+
+ dirty_hasIndirectUpstream() || dirty_isIndirectRoot -> {
+ if (snapshotIsDirect) {
+ downstreamSet.moveDirectUpstreamToIndirect(
+ coroutineScope,
+ scheduler,
+ oldDirectDepth = snapshotDirectDepth,
+ newIndirectDepth = dirty_indirectDepth,
+ newIndirectSet =
+ buildSet {
+ addAll(dirty_indirectUpstreamRoots)
+ if (dirty_isIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ )
+ } else {
+ downstreamSet.adjustIndirectUpstream(
+ coroutineScope,
+ scheduler,
+ oldDepth = snapshotIndirectDepth,
+ newDepth = dirty_indirectDepth,
+ removals =
+ buildSet {
+ addAll(indirectRemovals)
+ if (snapshotIsIndirectRoot && !dirty_isIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ additions =
+ buildSet {
+ addAll(indirectAdditions)
+ if (!snapshotIsIndirectRoot && dirty_isIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ )
+ }
+ }
+
+ else -> {
+ // die
+ muxNode.lifecycle.lifecycleState = MuxLifecycleState.Dead
+
+ if (snapshotIsDirect) {
+ downstreamSet.removeDirectUpstream(
+ coroutineScope,
+ scheduler,
+ depth = snapshotDirectDepth,
+ )
+ } else {
+ downstreamSet.removeIndirectUpstream(
+ coroutineScope,
+ scheduler,
+ depth = snapshotIndirectDepth,
+ indirectSet =
+ buildSet {
+ addAll(snapshotIndirectRoots)
+ if (snapshotIsIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ )
+ }
+ downstreamSet.clear()
+ }
+ }
+ reset()
+ }
+
+ fun dirty_hasDirectUpstream(): Boolean = dirty_directUpstreamDepths.isNotEmpty()
+
+ private fun dirty_hasIndirectUpstream(): Boolean = dirty_indirectUpstreamRoots.isNotEmpty()
+
+ override fun toString(): String =
+ "DepthTracker(" +
+ "sIsDirect=$snapshotIsDirect, " +
+ "sDirectDepth=$snapshotDirectDepth, " +
+ "sIndirectDepth=$snapshotIndirectDepth, " +
+ "sIndirectRoots=$snapshotIndirectRoots, " +
+ "dIsIndirectRoot=$dirty_isIndirectRoot, " +
+ "dDirectDepths=$dirty_directUpstreamDepths, " +
+ "dIndirectDepths=$dirty_indirectUpstreamDepths, " +
+ "dIndirectRoots=$dirty_indirectUpstreamRoots" +
+ ")"
+
+ fun reset() {
+ snapshotIsDirect = dirty_hasDirectUpstream()
+ snapshotDirectDepth = dirty_directDepth
+ snapshotIndirectDepth = dirty_indirectDepth
+ snapshotIsIndirectRoot = dirty_isIndirectRoot
+ if (indirectAdditions.isNotEmpty() || indirectRemovals.isNotEmpty()) {
+ _snapshotIndirectRoots.clear()
+ _snapshotIndirectRoots.addAll(dirty_indirectUpstreamRoots)
+ }
+ indirectAdditions.clear()
+ indirectRemovals.clear()
+ // check(!isDirty()) { "should not be dirty after a reset" }
+ }
+
+ fun isDirty(): Boolean =
+ when {
+ snapshotIsDirect -> !dirty_depthIsDirect || snapshotDirectDepth != dirty_directDepth
+ snapshotIsIndirectRoot -> dirty_depthIsDirect || !dirty_isIndirectRoot
+ else ->
+ dirty_depthIsDirect ||
+ dirty_isIndirectRoot ||
+ snapshotIndirectDepth != dirty_indirectDepth ||
+ indirectAdditions.isNotEmpty() ||
+ indirectRemovals.isNotEmpty()
+ }
+
+ fun dirty_depthIncreased(): Boolean =
+ snapshotDirectDepth < dirty_directDepth || snapshotIsIndirect && dirty_hasDirectUpstream()
+}
+
+/**
+ * Tracks downstream nodes to be scheduled when the owner of this DownstreamSet produces a value in
+ * a transaction.
+ */
+internal class DownstreamSet {
+
+ val outputs = HashSet<Output<*>>()
+ val stateWriters = mutableListOf<TStateSource<*>>()
+ val muxMovers = HashSet<MuxDeferredNode<*, *>>()
+ val nodes = HashSet<SchedulableNode>()
+
+ fun add(schedulable: Schedulable) {
+ when (schedulable) {
+ is Schedulable.S -> stateWriters.add(schedulable.state)
+ is Schedulable.M -> muxMovers.add(schedulable.muxMover)
+ is Schedulable.N -> nodes.add(schedulable.node)
+ is Schedulable.O -> outputs.add(schedulable.output)
+ }
+ }
+
+ fun remove(schedulable: Schedulable) {
+ when (schedulable) {
+ is Schedulable.S -> error("WTF: latches are never removed")
+ is Schedulable.M -> muxMovers.remove(schedulable.muxMover)
+ is Schedulable.N -> nodes.remove(schedulable.node)
+ is Schedulable.O -> outputs.remove(schedulable.output)
+ }
+ }
+
+ fun adjustDirectUpstream(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch { node.adjustDirectUpstream(scheduler, oldDepth, newDepth) }
+ }
+ }
+
+ fun moveIndirectUpstreamToDirect(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch {
+ node.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+ }
+ for (mover in muxMovers) {
+ launch {
+ mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet)
+ }
+ }
+ }
+
+ fun adjustIndirectUpstream(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch {
+ node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+ }
+ for (mover in muxMovers) {
+ launch {
+ mover.adjustIndirectPatchNode(
+ scheduler,
+ oldDepth,
+ newDepth,
+ removals,
+ additions,
+ )
+ }
+ }
+ }
+
+ fun moveDirectUpstreamToIndirect(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch {
+ node.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+ }
+ for (mover in muxMovers) {
+ launch {
+ mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet)
+ }
+ }
+ }
+
+ fun removeIndirectUpstream(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch { node.removeIndirectUpstream(scheduler, depth, indirectSet) }
+ }
+ for (mover in muxMovers) {
+ launch { mover.removeIndirectPatchNode(scheduler, depth, indirectSet) }
+ }
+ for (output in outputs) {
+ launch { output.kill() }
+ }
+ }
+
+ fun removeDirectUpstream(coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch { node.removeDirectUpstream(scheduler, depth) }
+ }
+ for (mover in muxMovers) {
+ launch { mover.removeDirectPatchNode(scheduler) }
+ }
+ for (output in outputs) {
+ launch { output.kill() }
+ }
+ }
+
+ fun clear() {
+ outputs.clear()
+ stateWriters.clear()
+ muxMovers.clear()
+ nodes.clear()
+ }
+}
+
+// TODO: remove this indirection
+internal sealed interface Schedulable {
+ data class S constructor(val state: TStateSource<*>) : Schedulable
+
+ data class M constructor(val muxMover: MuxDeferredNode<*, *>) : Schedulable
+
+ data class N constructor(val node: SchedulableNode) : Schedulable
+
+ data class O constructor(val output: Output<*>) : Schedulable
+}
+
+internal fun DownstreamSet.isEmpty() =
+ nodes.isEmpty() && outputs.isEmpty() && muxMovers.isEmpty() && stateWriters.isEmpty()
+
+@Suppress("NOTHING_TO_INLINE") internal inline fun DownstreamSet.isNotEmpty() = !isEmpty()
+
+internal fun CoroutineScope.scheduleAll(
+ downstreamSet: DownstreamSet,
+ evalScope: EvalScope,
+): Boolean {
+ downstreamSet.nodes.forEach { launch { it.schedule(evalScope) } }
+ downstreamSet.muxMovers.forEach { launch { it.scheduleMover(evalScope) } }
+ downstreamSet.outputs.forEach { launch { it.schedule(evalScope) } }
+ downstreamSet.stateWriters.forEach { evalScope.schedule(it) }
+ return downstreamSet.isNotEmpty()
+}
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
new file mode 100644
index 0000000..57db9a4
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.none
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Performs actions once, when the reactive component is first connected to the network. */
+internal class Init<out A>(val name: String?, private val block: suspend InitScope.() -> A) {
+
+ /** Has the initialization logic been evaluated yet? */
+ private val initialized = AtomicBoolean()
+
+ /**
+ * Stores the result after initialization, as well as the id of the [Network] it's been
+ * initialized with.
+ */
+ private val cache = CompletableDeferred<Pair<Any, A>>()
+
+ suspend fun connect(evalScope: InitScope): A =
+ if (initialized.getAndSet(true)) {
+ // Read from cache
+ val (networkId, result) = cache.await()
+ check(networkId == evalScope.networkId) { "Network mismatch" }
+ result
+ } else {
+ // Write to cache
+ block(evalScope).also { cache.complete(evalScope.networkId to it) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getUnsafe(): Maybe<A> =
+ if (cache.isCompleted) {
+ just(cache.getCompleted().second)
+ } else {
+ none
+ }
+}
+
+internal fun <A> init(name: String?, block: suspend InitScope.() -> A) = Init(name, block)
+
+internal fun <A> constInit(name: String?, value: A) = init(name) { value }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt
new file mode 100644
index 0000000..8efaf79
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.just
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+internal class InputNode<A>(
+ private val activate: suspend EvalScope.() -> Unit = {},
+ private val deactivate: () -> Unit = {},
+) : PushNode<A>, Key<A> {
+
+ internal val downstreamSet = DownstreamSet()
+ private val mutex = Mutex()
+ private val activated = AtomicBoolean(false)
+
+ override val depthTracker: DepthTracker = DepthTracker()
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ transactionStore.contains(this)
+
+ suspend fun visit(evalScope: EvalScope, value: A) {
+ evalScope.setResult(this, value)
+ coroutineScope {
+ if (!mutex.withLock { scheduleAll(downstreamSet, evalScope) }) {
+ evalScope.scheduleDeactivation(this@InputNode)
+ }
+ }
+ }
+
+ override suspend fun removeDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.remove(downstream) }
+ }
+
+ override suspend fun deactivateIfNeeded() {
+ if (mutex.withLock { downstreamSet.isEmpty() && activated.getAndSet(false) }) {
+ deactivate()
+ }
+ }
+
+ override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+
+ override suspend fun addDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.add(downstream) }
+ }
+
+ suspend fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) {
+ val needsActivation =
+ mutex.withLock {
+ val wasEmpty = downstreamSet.isEmpty()
+ downstreamSet.add(downstream)
+ wasEmpty && !activated.getAndSet(true)
+ }
+ if (needsActivation) {
+ activate(evalScope)
+ }
+ }
+
+ override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ val needsDeactivation =
+ mutex.withLock {
+ downstreamSet.remove(downstream)
+ downstreamSet.isEmpty() && activated.getAndSet(false)
+ }
+ if (needsDeactivation) {
+ deactivate()
+ }
+ }
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> =
+ evalScope.getCurrentValue(this)
+}
+
+internal fun <A> InputNode<A>.activated() = TFlowCheap { downstream ->
+ val input = this@activated
+ addDownstreamAndActivateIfNeeded(downstream, evalScope = this)
+ ActivationResult(connection = NodeConnection(input, input), needsEval = hasCurrentValue(input))
+}
+
+internal data object AlwaysNode : PushNode<Unit> {
+
+ override val depthTracker = DepthTracker()
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = true
+
+ override suspend fun removeDownstream(downstream: Schedulable) {}
+
+ override suspend fun deactivateIfNeeded() {}
+
+ override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {}
+
+ override suspend fun addDownstream(downstream: Schedulable) {}
+
+ override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {}
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Unit> = just(Unit)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt
new file mode 100644
index 0000000..af864e6
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.FrpBuildScope
+import com.android.systemui.kairos.FrpStateScope
+import com.android.systemui.kairos.FrpTransactionScope
+import com.android.systemui.kairos.TFlow
+import com.android.systemui.kairos.internal.util.HeteroMap
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.util.Maybe
+
+internal interface InitScope {
+ val networkId: Any
+}
+
+internal interface EvalScope : NetworkScope, DeferScope {
+ val frpScope: FrpTransactionScope
+
+ suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R
+}
+
+internal interface StateScope : EvalScope {
+ override val frpScope: FrpStateScope
+
+ suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R
+
+ val endSignal: TFlow<Any>
+
+ fun childStateScope(newEnd: TFlow<Any>): StateScope
+}
+
+internal interface BuildScope : StateScope {
+ override val frpScope: FrpBuildScope
+
+ suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R
+}
+
+internal interface NetworkScope : InitScope {
+
+ val epoch: Long
+ val network: Network
+
+ val compactor: Scheduler
+ val scheduler: Scheduler
+
+ val transactionStore: HeteroMap
+
+ fun scheduleOutput(output: Output<*>)
+
+ fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *>)
+
+ fun schedule(state: TStateSource<*>)
+
+ suspend fun schedule(node: MuxNode<*, *, *>)
+
+ fun scheduleDeactivation(node: PushNode<*>)
+
+ fun scheduleDeactivation(output: Output<*>)
+}
+
+internal fun <A> NetworkScope.setResult(node: Key<A>, result: A) {
+ transactionStore[node] = result
+}
+
+internal fun <A> NetworkScope.getCurrentValue(key: Key<A>): Maybe<A> = transactionStore[key]
+
+internal fun NetworkScope.hasCurrentValue(key: Key<*>): Boolean = transactionStore.contains(key)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt
new file mode 100644
index 0000000..f7ff15f
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.internal.util.ConcurrentNullableHashMap
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.util.Just
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/** Base class for muxing nodes, which have a potentially dynamic collection of upstream nodes. */
+internal sealed class MuxNode<K : Any, V, Output>(val lifecycle: MuxLifecycle<Output>) :
+ PushNode<Output> {
+
+ inline val mutex
+ get() = lifecycle.mutex
+
+ // TODO: preserve insertion order?
+ val upstreamData = ConcurrentNullableHashMap<K, V>()
+ val switchedIn = ConcurrentHashMap<K, MuxBranchNode<K, V>>()
+ val downstreamSet: DownstreamSet = DownstreamSet()
+
+ // TODO: inline DepthTracker? would need to be added to PushNode signature
+ final override val depthTracker = DepthTracker()
+
+ final override suspend fun addDownstream(downstream: Schedulable) {
+ mutex.withLock { addDownstreamLocked(downstream) }
+ }
+
+ /**
+ * Adds a downstream schedulable to this mux node, such that when this mux node emits a value,
+ * it will be scheduled for evaluation within this same transaction.
+ *
+ * Must only be called when [mutex] is acquired.
+ */
+ fun addDownstreamLocked(downstream: Schedulable) {
+ downstreamSet.add(downstream)
+ }
+
+ final override suspend fun removeDownstream(downstream: Schedulable) {
+ // TODO: return boolean?
+ mutex.withLock { downstreamSet.remove(downstream) }
+ }
+
+ final override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ val deactivate =
+ mutex.withLock {
+ downstreamSet.remove(downstream)
+ downstreamSet.isEmpty()
+ }
+ if (deactivate) {
+ doDeactivate()
+ }
+ }
+
+ final override suspend fun deactivateIfNeeded() {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ doDeactivate()
+ }
+ }
+
+ /** visit this node from the scheduler (push eval) */
+ abstract suspend fun visit(evalScope: EvalScope)
+
+ /** perform deactivation logic, propagating to all upstream nodes. */
+ protected abstract suspend fun doDeactivate()
+
+ final override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+
+ suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ mutex.withLock {
+ if (depthTracker.addDirectUpstream(oldDepth, newDepth)) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectRoots: Set<MuxDeferredNode<*, *>>,
+ newDepth: Int,
+ ) {
+ mutex.withLock {
+ if (
+ depthTracker.addDirectUpstream(oldDepth = null, newDepth) or
+ depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or
+ depthTracker.updateIndirectRoots(removals = oldIndirectRoots)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ mutex.withLock {
+ if (
+ depthTracker.addIndirectUpstream(oldDepth, newDepth) or
+ depthTracker.updateIndirectRoots(
+ additions,
+ removals,
+ butNot = this as? MuxDeferredNode<*, *>,
+ )
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ mutex.withLock {
+ if (
+ depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or
+ depthTracker.removeDirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(
+ additions = newIndirectSet,
+ butNot = this as? MuxDeferredNode<*, *>,
+ )
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) {
+ mutex.withLock {
+ switchedIn.remove(key)
+ if (depthTracker.removeDirectUpstream(depth)) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ key: K,
+ ) {
+ mutex.withLock {
+ switchedIn.remove(key)
+ if (
+ depthTracker.removeIndirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(removals = indirectSet)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun visitCompact(scheduler: Scheduler) = coroutineScope {
+ if (depthTracker.isDirty()) {
+ depthTracker.applyChanges(coroutineScope = this, scheduler, downstreamSet, this@MuxNode)
+ }
+ }
+
+ abstract fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean
+}
+
+/** An input branch of a mux node, associated with a key. */
+internal class MuxBranchNode<K : Any, V>(private val muxNode: MuxNode<K, V, *>, val key: K) :
+ SchedulableNode {
+
+ val schedulable = Schedulable.N(this)
+
+ @Volatile lateinit var upstream: NodeConnection<V>
+
+ override suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult = upstream.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ muxNode.upstreamData[key] = upstreamResult.value
+ evalScope.schedule(muxNode)
+ }
+ }
+
+ override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ muxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
+ }
+
+ override suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) {
+ muxNode.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+
+ override suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+
+ override suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+
+ override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ muxNode.removeDirectUpstream(scheduler, depth, key)
+ }
+
+ override suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.removeIndirectUpstream(scheduler, depth, indirectSet, key)
+ }
+
+ override fun toString(): String = "MuxBranchNode(key=$key, mux=$muxNode)"
+}
+
+/** Tracks lifecycle of MuxNode in the network. Essentially a mutable ref for MuxLifecycleState. */
+internal class MuxLifecycle<A>(@Volatile var lifecycleState: MuxLifecycleState<A>) : TFlowImpl<A> {
+ val mutex = Mutex()
+
+ override fun toString(): String = "TFlowLifecycle[$hashString][$lifecycleState][$mutex]"
+
+ override suspend fun activate(
+ evalScope: EvalScope,
+ downstream: Schedulable,
+ ): ActivationResult<A>? =
+ mutex.withLock {
+ when (val state = lifecycleState) {
+ is MuxLifecycleState.Dead -> null
+ is MuxLifecycleState.Active -> {
+ state.node.addDownstreamLocked(downstream)
+ ActivationResult(
+ connection = NodeConnection(state.node, state.node),
+ needsEval = state.node.hasCurrentValueLocked(evalScope.transactionStore),
+ )
+ }
+ is MuxLifecycleState.Inactive -> {
+ state.spec
+ .activate(evalScope, this@MuxLifecycle)
+ .also { node ->
+ lifecycleState =
+ if (node == null) {
+ MuxLifecycleState.Dead
+ } else {
+ MuxLifecycleState.Active(node)
+ }
+ }
+ ?.let { node ->
+ node.addDownstreamLocked(downstream)
+ ActivationResult(
+ connection = NodeConnection(node, node),
+ needsEval = false,
+ )
+ }
+ }
+ }
+ }
+}
+
+internal sealed interface MuxLifecycleState<out A> {
+ class Inactive<A>(val spec: MuxActivator<A>) : MuxLifecycleState<A> {
+ override fun toString(): String = "Inactive"
+ }
+
+ class Active<A>(val node: MuxNode<*, *, A>) : MuxLifecycleState<A> {
+ override fun toString(): String = "Active(node=$node)"
+ }
+
+ data object Dead : MuxLifecycleState<Nothing>
+}
+
+internal interface MuxActivator<A> {
+ suspend fun activate(evalScope: EvalScope, lifecycle: MuxLifecycle<A>): MuxNode<*, *, A>?
+}
+
+internal inline fun <A> MuxLifecycle(onSubscribe: MuxActivator<A>): TFlowImpl<A> =
+ MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe))
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
new file mode 100644
index 0000000..08bee85
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
@@ -0,0 +1,473 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.internal.util.associateByIndexTo
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.internal.util.mapParallel
+import com.android.systemui.kairos.internal.util.mapValuesNotNullParallelTo
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.None
+import com.android.systemui.kairos.util.Right
+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.merge
+import com.android.systemui.kairos.util.orElseGet
+import com.android.systemui.kairos.util.partitionEithers
+import com.android.systemui.kairos.util.these
+import java.util.TreeMap
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+
+internal class MuxDeferredNode<K : Any, V>(
+ lifecycle: MuxLifecycle<Map<K, V>>,
+ val spec: MuxActivator<Map<K, V>>,
+) : MuxNode<K, V, Map<K, V>>(lifecycle), Key<Map<K, V>> {
+
+ val schedulable = Schedulable.M(this)
+
+ @Volatile var patches: NodeConnection<Map<K, Maybe<TFlowImpl<V>>>>? = null
+ @Volatile var patchData: Map<K, Maybe<TFlowImpl<V>>>? = null
+
+ override fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean =
+ transactionStore.contains(this)
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ mutex.withLock { hasCurrentValueLocked(transactionStore) }
+
+ override suspend fun visit(evalScope: EvalScope) {
+ val result = upstreamData.toMap()
+ upstreamData.clear()
+ val scheduleDownstream = result.isNotEmpty()
+ val compactDownstream = depthTracker.isDirty()
+ if (scheduleDownstream || compactDownstream) {
+ coroutineScope {
+ mutex.withLock {
+ if (compactDownstream) {
+ depthTracker.applyChanges(
+ coroutineScope = this,
+ evalScope.scheduler,
+ downstreamSet,
+ muxNode = this@MuxDeferredNode,
+ )
+ }
+ if (scheduleDownstream) {
+ evalScope.setResult(this@MuxDeferredNode, result)
+ if (!scheduleAll(downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@MuxDeferredNode)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Map<K, V>> =
+ evalScope.getCurrentValue(key = this)
+
+ private suspend fun compactIfNeeded(evalScope: EvalScope) {
+ depthTracker.propagateChanges(evalScope.compactor, this)
+ }
+
+ override suspend fun doDeactivate() {
+ // Update lifecycle
+ lifecycle.mutex.withLock {
+ if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
+ lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
+ }
+ // Process branch nodes
+ coroutineScope {
+ switchedIn.values.forEach { branchNode ->
+ branchNode.upstream.let {
+ launch { it.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) }
+ }
+ }
+ }
+ // Process patch node
+ patches?.removeDownstreamAndDeactivateIfNeeded(schedulable)
+ }
+
+ // MOVE phase
+ // - concurrent moves may be occurring, but no more evals. all depth recalculations are
+ // deferred to the end of this phase.
+ suspend fun performMove(evalScope: EvalScope) {
+ val patch = patchData ?: return
+ patchData = null
+
+ // TODO: this logic is very similar to what's in MuxPromptMoving, maybe turn into an inline
+ // fun?
+
+ // We have a patch, process additions/updates and removals
+ val (adds, removes) =
+ patch
+ .asSequence()
+ .map { (k, newUpstream: Maybe<TFlowImpl<V>>) ->
+ when (newUpstream) {
+ is Just -> Left(k to newUpstream.value)
+ None -> Right(k)
+ }
+ }
+ .partitionEithers()
+
+ val severed = mutableListOf<NodeConnection<*>>()
+
+ coroutineScope {
+ // remove and sever
+ removes.forEach { k ->
+ switchedIn.remove(k)?.let { branchNode: MuxBranchNode<K, V> ->
+ val conn = branchNode.upstream
+ severed.add(conn)
+ launch { conn.removeDownstream(downstream = branchNode.schedulable) }
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+ }
+
+ // add or replace
+ adds
+ .mapParallel { (k, newUpstream: TFlowImpl<V>) ->
+ val branchNode = MuxBranchNode(this@MuxDeferredNode, k)
+ k to
+ newUpstream.activate(evalScope, branchNode.schedulable)?.let { (conn, _) ->
+ branchNode.apply { upstream = conn }
+ }
+ }
+ .forEach { (k, newBranch: MuxBranchNode<K, V>?) ->
+ // remove old and sever, if present
+ switchedIn.remove(k)?.let { branchNode ->
+ val conn = branchNode.upstream
+ severed.add(conn)
+ launch { conn.removeDownstream(downstream = branchNode.schedulable) }
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+
+ // add new
+ newBranch?.let {
+ switchedIn[k] = newBranch
+ val branchDepthTracker = newBranch.upstream.depthTracker
+ if (branchDepthTracker.snapshotIsDirect) {
+ depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotDirectDepth,
+ )
+ } else {
+ depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotIndirectDepth,
+ )
+ depthTracker.updateIndirectRoots(
+ additions = branchDepthTracker.snapshotIndirectRoots,
+ butNot = this@MuxDeferredNode,
+ )
+ }
+ }
+ }
+ }
+
+ coroutineScope {
+ for (severedNode in severed) {
+ launch { severedNode.scheduleDeactivationIfNeeded(evalScope) }
+ }
+ }
+
+ compactIfNeeded(evalScope)
+ }
+
+ suspend fun removeDirectPatchNode(scheduler: Scheduler) {
+ mutex.withLock {
+ if (
+ depthTracker.removeIndirectUpstream(depth = 0) or
+ depthTracker.setIsIndirectRoot(false)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ patches = null
+ }
+ }
+
+ suspend fun removeIndirectPatchNode(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // indirectly connected patches forward the indirectSet
+ mutex.withLock {
+ if (
+ depthTracker.updateIndirectRoots(removals = indirectSet) or
+ depthTracker.removeIndirectUpstream(depth)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ patches = null
+ }
+ }
+
+ suspend fun moveIndirectPatchNodeToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // directly connected patches are stored as an indirect singleton set of the patchNode
+ mutex.withLock {
+ if (
+ depthTracker.updateIndirectRoots(removals = oldIndirectSet) or
+ depthTracker.removeIndirectUpstream(oldIndirectDepth) or
+ depthTracker.setIsIndirectRoot(true)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun moveDirectPatchNodeToIndirect(
+ scheduler: Scheduler,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // indirectly connected patches forward the indirectSet
+ mutex.withLock {
+ if (
+ depthTracker.setIsIndirectRoot(false) or
+ depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or
+ depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun adjustIndirectPatchNode(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // indirectly connected patches forward the indirectSet
+ mutex.withLock {
+ if (
+ depthTracker.updateIndirectRoots(
+ additions = additions,
+ removals = removals,
+ butNot = this,
+ ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun scheduleMover(evalScope: EvalScope) {
+ patchData =
+ checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" }
+ .getPushEvent(evalScope)
+ .orElseGet { null }
+ evalScope.scheduleMuxMover(this)
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal inline fun <A> switchDeferredImplSingle(
+ crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>,
+): TFlowImpl<A> =
+ mapImpl({
+ switchDeferredImpl(
+ getStorage = { mapOf(Unit to getStorage()) },
+ getPatches = { mapImpl(getPatches) { newFlow -> mapOf(Unit to just(newFlow)) } },
+ )
+ }) { map ->
+ map.getValue(Unit)
+ }
+
+internal fun <K : Any, A> switchDeferredImpl(
+ getStorage: suspend EvalScope.() -> Map<K, TFlowImpl<A>>,
+ getPatches: suspend EvalScope.() -> TFlowImpl<Map<K, Maybe<TFlowImpl<A>>>>,
+): TFlowImpl<Map<K, A>> =
+ MuxLifecycle(
+ object : MuxActivator<Map<K, A>> {
+ override suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: MuxLifecycle<Map<K, A>>,
+ ): MuxNode<*, *, Map<K, A>>? {
+ val storage: Map<K, TFlowImpl<A>> = getStorage(evalScope)
+ // Initialize mux node and switched-in connections.
+ val muxNode =
+ MuxDeferredNode(lifecycle, this).apply {
+ storage.mapValuesNotNullParallelTo(switchedIn) { (key, flow) ->
+ val branchNode = MuxBranchNode(this@apply, key)
+ flow.activate(evalScope, branchNode.schedulable)?.let {
+ (conn, needsEval) ->
+ branchNode
+ .apply { upstream = conn }
+ .also {
+ if (needsEval) {
+ val result = conn.getPushEvent(evalScope)
+ if (result is Just) {
+ upstreamData[key] = result.value
+ }
+ }
+ }
+ }
+ }
+ }
+ // Update depth based on all initial switched-in nodes.
+ muxNode.switchedIn.values.forEach { branch ->
+ val conn = branch.upstream
+ if (conn.depthTracker.snapshotIsDirect) {
+ muxNode.depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotDirectDepth,
+ )
+ } else {
+ muxNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotIndirectDepth,
+ )
+ muxNode.depthTracker.updateIndirectRoots(
+ additions = conn.depthTracker.snapshotIndirectRoots,
+ butNot = muxNode,
+ )
+ }
+ }
+ // We don't have our patches connection established yet, so for now pretend we have
+ // a direct connection to patches. We will update downstream nodes later if this
+ // turns out to be a lie.
+ muxNode.depthTracker.setIsIndirectRoot(true)
+ muxNode.depthTracker.reset()
+
+ // Setup patches connection; deferring allows for a recursive connection, where
+ // muxNode is downstream of itself via patches.
+ var isIndirect = true
+ evalScope.deferAction {
+ val (patchesConn, needsEval) =
+ getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable)
+ ?: run {
+ isIndirect = false
+ // Turns out we can't connect to patches, so update our depth and
+ // propagate
+ muxNode.mutex.withLock {
+ if (muxNode.depthTracker.setIsIndirectRoot(false)) {
+ muxNode.depthTracker.schedule(evalScope.scheduler, muxNode)
+ }
+ }
+ return@deferAction
+ }
+ muxNode.patches = patchesConn
+
+ if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) {
+ // Turns out patches is indirect, so we are not a root. Update depth and
+ // propagate.
+ muxNode.mutex.withLock {
+ if (
+ muxNode.depthTracker.setIsIndirectRoot(false) or
+ muxNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = patchesConn.depthTracker.snapshotIndirectDepth,
+ ) or
+ muxNode.depthTracker.updateIndirectRoots(
+ additions = patchesConn.depthTracker.snapshotIndirectRoots
+ )
+ ) {
+ muxNode.depthTracker.schedule(evalScope.scheduler, muxNode)
+ }
+ }
+ }
+ // Schedule mover to process patch emission at the end of this transaction, if
+ // needed.
+ if (needsEval) {
+ val result = patchesConn.getPushEvent(evalScope)
+ if (result is Just) {
+ muxNode.patchData = result.value
+ evalScope.scheduleMuxMover(muxNode)
+ }
+ }
+ }
+
+ // Schedule for evaluation if any switched-in nodes have already emitted within
+ // this transaction.
+ if (muxNode.upstreamData.isNotEmpty()) {
+ evalScope.schedule(muxNode)
+ }
+ return muxNode.takeUnless { muxNode.switchedIn.isEmpty() && !isIndirect }
+ }
+ }
+ )
+
+internal inline fun <A> mergeNodes(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline getOther: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline f: suspend EvalScope.(A, A) -> A,
+): TFlowImpl<A> {
+ val merged =
+ mapImpl({ mergeNodes(getPulse, getOther) }) { these ->
+ these.merge { thiz, that -> f(thiz, that) }
+ }
+ return merged.cached()
+}
+
+internal inline fun <A, B> mergeNodes(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline getOther: suspend EvalScope.() -> TFlowImpl<B>,
+): TFlowImpl<These<A, B>> {
+ val storage =
+ mapOf(
+ 0 to mapImpl(getPulse) { These.thiz<A, B>(it) },
+ 1 to mapImpl(getOther) { These.that(it) },
+ )
+ val switchNode = switchDeferredImpl(getStorage = { storage }, getPatches = { neverImpl })
+ val merged =
+ mapImpl({ switchNode }) { mergeResults ->
+ val first = mergeResults.getMaybe(0).flatMap { it.maybeThis() }
+ val second = mergeResults.getMaybe(1).flatMap { it.maybeThat() }
+ these(first, second).orElseGet { error("unexpected missing merge result") }
+ }
+ return merged.cached()
+}
+
+internal inline fun <A> mergeNodes(
+ crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>>
+): TFlowImpl<List<A>> {
+ val switchNode =
+ switchDeferredImpl(
+ getStorage = { getPulses().associateByIndexTo(TreeMap()) },
+ getPatches = { neverImpl },
+ )
+ val merged = mapImpl({ switchNode }) { mergeResults -> mergeResults.values.toList() }
+ return merged.cached()
+}
+
+internal inline fun <A> mergeNodesLeft(
+ crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>>
+): TFlowImpl<A> {
+ val switchNode =
+ switchDeferredImpl(
+ getStorage = { getPulses().associateByIndexTo(TreeMap()) },
+ getPatches = { neverImpl },
+ )
+ val merged =
+ mapImpl({ switchNode }) { mergeResults: Map<Int, A> -> mergeResults.values.first() }
+ 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
new file mode 100644
index 0000000..cdfafa9
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
@@ -0,0 +1,472 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.internal.util.launchImmediate
+import com.android.systemui.kairos.internal.util.mapParallel
+import com.android.systemui.kairos.internal.util.mapValuesNotNullParallelTo
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.None
+import com.android.systemui.kairos.util.Right
+import com.android.systemui.kairos.util.filterJust
+import com.android.systemui.kairos.util.map
+import com.android.systemui.kairos.util.partitionEithers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+
+internal class MuxPromptMovingNode<K : Any, V>(
+ lifecycle: MuxLifecycle<Pair<Map<K, V>, Map<K, PullNode<V>>?>>,
+ private val spec: MuxActivator<Pair<Map<K, V>, Map<K, PullNode<V>>?>>,
+) :
+ MuxNode<K, V, Pair<Map<K, V>, Map<K, PullNode<V>>?>>(lifecycle),
+ Key<Pair<Map<K, V>, Map<K, PullNode<V>>?>> {
+
+ @Volatile var patchData: Map<K, Maybe<TFlowImpl<V>>>? = null
+ @Volatile var patches: MuxPromptPatchNode<K, V>? = null
+
+ @Volatile private var reEval: Pair<Map<K, V>, Map<K, PullNode<V>>?>? = null
+
+ override fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean =
+ transactionStore.contains(this)
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ mutex.withLock { hasCurrentValueLocked(transactionStore) }
+
+ override suspend fun visit(evalScope: EvalScope) {
+ val preSwitchResults: Map<K, V> = upstreamData.toMap()
+ upstreamData.clear()
+
+ val patch: Map<K, Maybe<TFlowImpl<V>>>? = patchData
+ patchData = null
+
+ val (reschedule, evalResult) =
+ reEval?.let { false to it }
+ ?: if (preSwitchResults.isNotEmpty() || patch?.isNotEmpty() == true) {
+ doEval(preSwitchResults, patch, evalScope)
+ } else {
+ false to null
+ }
+ reEval = null
+
+ if (reschedule || depthTracker.dirty_depthIncreased()) {
+ reEval = evalResult
+ // Can't schedule downstream yet, need to compact first
+ if (depthTracker.dirty_depthIncreased()) {
+ depthTracker.schedule(evalScope.compactor, node = this)
+ }
+ evalScope.schedule(this)
+ } else {
+ val compactDownstream = depthTracker.isDirty()
+ if (evalResult != null || compactDownstream) {
+ coroutineScope {
+ mutex.withLock {
+ if (compactDownstream) {
+ adjustDownstreamDepths(evalScope, coroutineScope = this)
+ }
+ if (evalResult != null) {
+ evalScope.setResult(this@MuxPromptMovingNode, evalResult)
+ if (!scheduleAll(downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@MuxPromptMovingNode)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun doEval(
+ preSwitchResults: Map<K, V>,
+ patch: Map<K, Maybe<TFlowImpl<V>>>?,
+ evalScope: EvalScope,
+ ): Pair<Boolean, Pair<Map<K, V>, Map<K, PullNode<V>>?>?> {
+ val newlySwitchedIn: Map<K, PullNode<V>>? =
+ patch?.let {
+ // We have a patch, process additions/updates and removals
+ val (adds, removes) =
+ patch
+ .asSequence()
+ .map { (k, newUpstream: Maybe<TFlowImpl<V>>) ->
+ when (newUpstream) {
+ is Just -> Left(k to newUpstream.value)
+ None -> Right(k)
+ }
+ }
+ .partitionEithers()
+
+ val additionsAndUpdates = mutableMapOf<K, PullNode<V>>()
+ val severed = mutableListOf<NodeConnection<*>>()
+
+ coroutineScope {
+ // remove and sever
+ removes.forEach { k ->
+ switchedIn.remove(k)?.let { branchNode: MuxBranchNode<K, V> ->
+ val conn: NodeConnection<V> = branchNode.upstream
+ severed.add(conn)
+ launchImmediate {
+ conn.removeDownstream(downstream = branchNode.schedulable)
+ }
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+ }
+
+ // add or replace
+ adds
+ .mapParallel { (k, newUpstream: TFlowImpl<V>) ->
+ val branchNode = MuxBranchNode(this@MuxPromptMovingNode, k)
+ k to
+ newUpstream.activate(evalScope, branchNode.schedulable)?.let {
+ (conn, _) ->
+ branchNode.apply { upstream = conn }
+ }
+ }
+ .forEach { (k, newBranch: MuxBranchNode<K, V>?) ->
+ // remove old and sever, if present
+ switchedIn.remove(k)?.let { oldBranch: MuxBranchNode<K, V> ->
+ val conn: NodeConnection<V> = oldBranch.upstream
+ severed.add(conn)
+ launchImmediate {
+ conn.removeDownstream(downstream = oldBranch.schedulable)
+ }
+ depthTracker.removeDirectUpstream(
+ conn.depthTracker.snapshotDirectDepth
+ )
+ }
+
+ // add new
+ newBranch?.let {
+ switchedIn[k] = newBranch
+ additionsAndUpdates[k] = newBranch.upstream.directUpstream
+ val branchDepthTracker = newBranch.upstream.depthTracker
+ if (branchDepthTracker.snapshotIsDirect) {
+ depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotDirectDepth,
+ )
+ } else {
+ depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotIndirectDepth,
+ )
+ depthTracker.updateIndirectRoots(
+ additions = branchDepthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
+ }
+ }
+ }
+ }
+
+ coroutineScope {
+ for (severedNode in severed) {
+ launch { severedNode.scheduleDeactivationIfNeeded(evalScope) }
+ }
+ }
+
+ additionsAndUpdates.takeIf { it.isNotEmpty() }
+ }
+
+ return if (preSwitchResults.isNotEmpty() || newlySwitchedIn != null) {
+ (newlySwitchedIn != null) to (preSwitchResults to newlySwitchedIn)
+ } else {
+ false to null
+ }
+ }
+
+ private suspend fun adjustDownstreamDepths(
+ evalScope: EvalScope,
+ coroutineScope: CoroutineScope,
+ ) {
+ if (depthTracker.dirty_depthIncreased()) {
+ // schedule downstream nodes on the compaction scheduler; this scheduler is drained at
+ // the end of this eval depth, so that all depth increases are applied before we advance
+ // the eval step
+ depthTracker.schedule(evalScope.compactor, node = this@MuxPromptMovingNode)
+ } else if (depthTracker.isDirty()) {
+ // schedule downstream nodes on the eval scheduler; this is more efficient and is only
+ // safe if the depth hasn't increased
+ depthTracker.applyChanges(
+ coroutineScope,
+ evalScope.scheduler,
+ downstreamSet,
+ muxNode = this@MuxPromptMovingNode,
+ )
+ }
+ }
+
+ override suspend fun getPushEvent(
+ evalScope: EvalScope
+ ): Maybe<Pair<Map<K, V>, Map<K, PullNode<V>>?>> = evalScope.getCurrentValue(key = this)
+
+ override suspend fun doDeactivate() {
+ // Update lifecycle
+ lifecycle.mutex.withLock {
+ if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
+ lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
+ }
+ // Process branch nodes
+ switchedIn.values.forEach { branchNode ->
+ branchNode.upstream.removeDownstreamAndDeactivateIfNeeded(
+ downstream = branchNode.schedulable
+ )
+ }
+ // Process patch node
+ patches?.let { patches ->
+ patches.upstream.removeDownstreamAndDeactivateIfNeeded(downstream = patches.schedulable)
+ }
+ }
+
+ suspend fun removeIndirectPatchNode(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ mutex.withLock {
+ patches = null
+ if (
+ depthTracker.removeIndirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(removals = indirectSet)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) {
+ mutex.withLock {
+ patches = null
+ if (depthTracker.removeDirectUpstream(depth)) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+}
+
+internal class MuxPromptEvalNode<K, V>(
+ private val movingNode: PullNode<Pair<Map<K, V>, Map<K, PullNode<V>>?>>
+) : PullNode<Map<K, V>> {
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Map<K, V>> =
+ movingNode.getPushEvent(evalScope).map { (preSwitchResults, newlySwitchedIn) ->
+ coroutineScope {
+ newlySwitchedIn
+ ?.map { (k, v) -> async { v.getPushEvent(evalScope).map { k to it } } }
+ ?.awaitAll()
+ ?.asSequence()
+ ?.filterJust()
+ ?.toMap(preSwitchResults.toMutableMap()) ?: preSwitchResults
+ }
+ }
+}
+
+// TODO: inner class?
+internal class MuxPromptPatchNode<K : Any, V>(private val muxNode: MuxPromptMovingNode<K, V>) :
+ SchedulableNode {
+
+ val schedulable = Schedulable.N(this)
+
+ lateinit var upstream: NodeConnection<Map<K, Maybe<TFlowImpl<V>>>>
+
+ override suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult = upstream.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ muxNode.patchData = upstreamResult.value
+ evalScope.schedule(muxNode)
+ }
+ }
+
+ override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ muxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
+ }
+
+ override suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) {
+ muxNode.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+
+ override suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+
+ override suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+
+ override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ muxNode.removeDirectPatchNode(scheduler, depth)
+ }
+
+ override suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.removeIndirectPatchNode(scheduler, depth, indirectSet)
+ }
+}
+
+internal fun <K : Any, A> switchPromptImpl(
+ getStorage: suspend EvalScope.() -> Map<K, TFlowImpl<A>>,
+ getPatches: suspend EvalScope.() -> TFlowImpl<Map<K, Maybe<TFlowImpl<A>>>>,
+): TFlowImpl<Map<K, A>> {
+ val moving =
+ MuxLifecycle(
+ object : MuxActivator<Pair<Map<K, A>, Map<K, PullNode<A>>?>> {
+ override suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: MuxLifecycle<Pair<Map<K, A>, Map<K, PullNode<A>>?>>,
+ ): MuxNode<*, *, Pair<Map<K, A>, Map<K, PullNode<A>>?>>? {
+ val storage: Map<K, TFlowImpl<A>> = getStorage(evalScope)
+ // Initialize mux node and switched-in connections.
+ val movingNode =
+ MuxPromptMovingNode(lifecycle, this).apply {
+ coroutineScope {
+ launch {
+ storage.mapValuesNotNullParallelTo(switchedIn) { (key, flow) ->
+ val branchNode = MuxBranchNode(this@apply, key)
+ flow
+ .activate(
+ evalScope = evalScope,
+ downstream = branchNode.schedulable,
+ )
+ ?.let { (conn, needsEval) ->
+ branchNode
+ .apply { upstream = conn }
+ .also {
+ if (needsEval) {
+ val result =
+ conn.getPushEvent(evalScope)
+ if (result is Just) {
+ upstreamData[key] = result.value
+ }
+ }
+ }
+ }
+ }
+ }
+ // Setup patches connection
+ val patchNode = MuxPromptPatchNode(this@apply)
+ getPatches(evalScope)
+ .activate(
+ evalScope = evalScope,
+ downstream = patchNode.schedulable,
+ )
+ ?.let { (conn, needsEval) ->
+ patchNode.upstream = conn
+ patches = patchNode
+
+ if (needsEval) {
+ val result = conn.getPushEvent(evalScope)
+ if (result is Just) {
+ patchData = result.value
+ }
+ }
+ }
+ }
+ }
+ // Update depth based on all initial switched-in nodes.
+ movingNode.switchedIn.values.forEach { branch ->
+ val conn = branch.upstream
+ if (conn.depthTracker.snapshotIsDirect) {
+ movingNode.depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotDirectDepth,
+ )
+ } else {
+ movingNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotIndirectDepth,
+ )
+ movingNode.depthTracker.updateIndirectRoots(
+ additions = conn.depthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
+ }
+ }
+ // Update depth based on patches node.
+ movingNode.patches?.upstream?.let { conn ->
+ if (conn.depthTracker.snapshotIsDirect) {
+ movingNode.depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotDirectDepth,
+ )
+ } else {
+ movingNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotIndirectDepth,
+ )
+ movingNode.depthTracker.updateIndirectRoots(
+ additions = conn.depthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
+ }
+ }
+ movingNode.depthTracker.reset()
+
+ // Schedule for evaluation if any switched-in nodes or the patches node have
+ // already emitted within this transaction.
+ if (movingNode.patchData != null || movingNode.upstreamData.isNotEmpty()) {
+ evalScope.schedule(movingNode)
+ }
+
+ return movingNode.takeUnless { it.patches == null && it.switchedIn.isEmpty() }
+ }
+ }
+ )
+
+ val eval = TFlowCheap { downstream ->
+ moving.activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
+ val evalNode = MuxPromptEvalNode(connection.directUpstream)
+ ActivationResult(
+ connection = NodeConnection(evalNode, connection.schedulerUpstream),
+ needsEval = needsEval,
+ )
+ }
+ }
+ return eval.cached()
+}
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
new file mode 100644
index 0000000..f0df89d
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.TState
+import com.android.systemui.kairos.internal.util.HeteroMap
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.none
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedDeque
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.atomic.AtomicLong
+import kotlin.coroutines.ContinuationInterceptor
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.yield
+
+private val nextNetworkId = AtomicLong()
+
+internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
+
+ override val networkId: Any = nextNetworkId.getAndIncrement()
+
+ @Volatile
+ override var epoch: Long = 0L
+ private set
+
+ override val network
+ get() = this
+
+ override val compactor = SchedulerImpl()
+ override val scheduler = SchedulerImpl()
+ override val transactionStore = HeteroMap()
+
+ private val stateWrites = ConcurrentLinkedQueue<TStateSource<*>>()
+ private val outputsByDispatcher =
+ ConcurrentHashMap<ContinuationInterceptor, ConcurrentLinkedQueue<Output<*>>>()
+ private val muxMovers = ConcurrentLinkedQueue<MuxDeferredNode<*, *>>()
+ private val deactivations = ConcurrentLinkedDeque<PushNode<*>>()
+ private val outputDeactivations = ConcurrentLinkedQueue<Output<*>>()
+ private val transactionMutex = Mutex()
+ private val inputScheduleChan = Channel<ScheduledAction<*>>()
+
+ override fun scheduleOutput(output: Output<*>) {
+ val continuationInterceptor =
+ output.context[ContinuationInterceptor] ?: Dispatchers.Unconfined
+ outputsByDispatcher
+ .computeIfAbsent(continuationInterceptor) { ConcurrentLinkedQueue() }
+ .add(output)
+ }
+
+ override fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *>) {
+ muxMovers.add(muxMover)
+ }
+
+ override fun schedule(state: TStateSource<*>) {
+ stateWrites.add(state)
+ }
+
+ // TODO: weird that we have this *and* scheduler exposed
+ override suspend fun schedule(node: MuxNode<*, *, *>) {
+ scheduler.schedule(node.depthTracker.dirty_directDepth, node)
+ }
+
+ override fun scheduleDeactivation(node: PushNode<*>) {
+ deactivations.add(node)
+ }
+
+ override fun scheduleDeactivation(output: Output<*>) {
+ outputDeactivations.add(output)
+ }
+
+ /** Listens for external events and starts FRP transactions. Runs forever. */
+ suspend fun runInputScheduler() = coroutineScope {
+ launch { scheduler.activate() }
+ launch { compactor.activate() }
+ val actions = mutableListOf<ScheduledAction<*>>()
+ for (first in inputScheduleChan) {
+ // Drain and conflate all transaction requests into a single transaction
+ actions.add(first)
+ while (true) {
+ yield()
+ val func = inputScheduleChan.tryReceive().getOrNull() ?: break
+ actions.add(func)
+ }
+ transactionMutex.withLock {
+ // Run all actions
+ evalScope {
+ for (action in actions) {
+ launch { action.started(evalScope = this@evalScope) }
+ }
+ }
+ // Step through the network
+ doTransaction()
+ // Signal completion
+ while (actions.isNotEmpty()) {
+ actions.removeLast().completed()
+ }
+ }
+ }
+ }
+
+ /** Evaluates [block] inside of a new transaction when the network is ready. */
+ fun <R> transaction(block: suspend EvalScope.() -> R): Deferred<R> =
+ CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult ->
+ val job =
+ coroutineScope.launch {
+ inputScheduleChan.send(
+ ScheduledAction(onStartTransaction = block, onResult = onResult)
+ )
+ }
+ onResult.invokeOnCompletion { job.cancel() }
+ }
+
+ suspend fun <R> evalScope(block: suspend EvalScope.() -> R): R = deferScope {
+ block(EvalScopeImpl(this@Network, this))
+ }
+
+ /** Performs a transactional update of the FRP network. */
+ private suspend fun doTransaction() {
+ // Traverse network, then run outputs
+ do {
+ scheduler.drainEval(this)
+ } while (evalScope { evalOutputs(this) })
+ // Update states
+ evalScope { evalStateWriters(this) }
+ transactionStore.clear()
+ // Perform deferred switches
+ evalScope { evalMuxMovers(this) }
+ // Compact depths
+ scheduler.drainCompact()
+ compactor.drainCompact()
+ // Deactivate nodes with no downstream
+ evalDeactivations()
+ epoch++
+ }
+
+ /** Invokes all [Output]s that have received data within this transaction. */
+ private suspend fun evalOutputs(evalScope: EvalScope): Boolean {
+ // Outputs can enqueue other outputs, so we need two loops
+ if (outputsByDispatcher.isEmpty()) return false
+ while (outputsByDispatcher.isNotEmpty()) {
+ var launchedAny = false
+ coroutineScope {
+ for ((key, outputs) in outputsByDispatcher) {
+ if (outputs.isNotEmpty()) {
+ launchedAny = true
+ launch(key) {
+ while (outputs.isNotEmpty()) {
+ val output = outputs.remove()
+ launch { output.visit(evalScope) }
+ }
+ }
+ }
+ }
+ }
+ if (!launchedAny) outputsByDispatcher.clear()
+ }
+ return true
+ }
+
+ private suspend fun evalMuxMovers(evalScope: EvalScope) {
+ while (muxMovers.isNotEmpty()) {
+ coroutineScope {
+ val toMove = muxMovers.remove()
+ launch { toMove.performMove(evalScope) }
+ }
+ }
+ }
+
+ /** Updates all [TState]es that have changed within this transaction. */
+ private suspend fun evalStateWriters(evalScope: EvalScope) {
+ coroutineScope {
+ while (stateWrites.isNotEmpty()) {
+ val latch = stateWrites.remove()
+ launch { latch.updateState(evalScope) }
+ }
+ }
+ }
+
+ private suspend fun evalDeactivations() {
+ coroutineScope {
+ launch {
+ while (deactivations.isNotEmpty()) {
+ // traverse in reverse order
+ // - deactivations are added in depth-order during the node traversal phase
+ // - perform deactivations in reverse order, in case later ones propagate to
+ // earlier ones
+ val toDeactivate = deactivations.removeLast()
+ launch { toDeactivate.deactivateIfNeeded() }
+ }
+ }
+ while (outputDeactivations.isNotEmpty()) {
+ val toDeactivate = outputDeactivations.remove()
+ launch {
+ toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded(
+ downstream = toDeactivate.schedulable
+ )
+ }
+ }
+ }
+ check(deactivations.isEmpty()) { "unexpected lingering deactivations" }
+ check(outputDeactivations.isEmpty()) { "unexpected lingering output deactivations" }
+ }
+}
+
+internal class ScheduledAction<T>(
+ private val onResult: CompletableDeferred<T>? = null,
+ private val onStartTransaction: suspend EvalScope.() -> T,
+) {
+ private var result: Maybe<T> = none
+
+ suspend fun started(evalScope: EvalScope) {
+ result = just(onStartTransaction(evalScope))
+ }
+
+ fun completed() {
+ if (onResult != null) {
+ when (val result = result) {
+ is Just -> onResult.complete(result.value)
+ else -> {}
+ }
+ }
+ result = none
+ }
+}
+
+internal typealias TransactionStore = HeteroMap
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt
new file mode 100644
index 0000000..fbd9689
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.FrpScope
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal object NoScope {
+ private object FrpScopeImpl : FrpScope
+
+ suspend fun <R> runInFrpScope(block: suspend FrpScope.() -> R): R {
+ val complete = CompletableDeferred<R>(coroutineContext.job)
+ block.startCoroutine(
+ FrpScopeImpl,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt
new file mode 100644
index 0000000..0002407
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.util.Maybe
+
+/*
+Dmux
+Muxes + Branch
+*/
+internal sealed interface SchedulableNode {
+ /** schedule this node w/ given NodeEvalScope */
+ suspend fun schedule(evalScope: EvalScope)
+
+ suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int)
+
+ suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ )
+
+ suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ )
+
+ suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ )
+
+ suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ )
+
+ suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int)
+}
+
+/*
+All but Dmux
+ */
+internal sealed interface PullNode<out A> {
+ /**
+ * query the result of this node within the current transaction. if the node is cached, this
+ * will read from the cache, otherwise it will perform a full evaluation, even if invoked
+ * multiple times within a transaction.
+ */
+ suspend fun getPushEvent(evalScope: EvalScope): Maybe<A>
+}
+
+/*
+Muxes + DmuxBranch
+ */
+internal sealed interface PushNode<A> : PullNode<A> {
+
+ suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean
+
+ val depthTracker: DepthTracker
+
+ suspend fun removeDownstream(downstream: Schedulable)
+
+ /** called during cleanup phase */
+ suspend fun deactivateIfNeeded()
+
+ /** called from mux nodes after severs */
+ suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope)
+
+ suspend fun addDownstream(downstream: Schedulable)
+
+ suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt
new file mode 100644
index 0000000..a3af2d3
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.util.Just
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+internal class Output<A>(
+ val context: CoroutineContext = EmptyCoroutineContext,
+ val onDeath: suspend () -> Unit = {},
+ val onEmit: suspend EvalScope.(A) -> Unit,
+) {
+
+ val schedulable = Schedulable.O(this)
+
+ @Volatile var upstream: NodeConnection<A>? = null
+ @Volatile var result: Any? = NoResult
+
+ private object NoResult
+
+ // invoked by network
+ suspend fun visit(evalScope: EvalScope) {
+ val upstreamResult = result
+ check(upstreamResult !== NoResult) { "output visited with null upstream result" }
+ result = null
+ @Suppress("UNCHECKED_CAST") evalScope.onEmit(upstreamResult as A)
+ }
+
+ suspend fun kill() {
+ onDeath()
+ }
+
+ suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult =
+ checkNotNull(upstream) { "output scheduled with null upstream" }.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ result = upstreamResult.value
+ evalScope.scheduleOutput(this)
+ }
+ }
+}
+
+internal inline fun OneShot(crossinline onEmit: suspend EvalScope.() -> Unit): Output<Unit> =
+ Output<Unit>(onEmit = { onEmit() }).apply { result = Unit }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt
new file mode 100644
index 0000000..dac98e0
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.map
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+
+internal val neverImpl: TFlowImpl<Nothing> = TFlowCheap { null }
+
+internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: suspend EvalScope.(A) -> B) :
+ PullNode<B> {
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<B> =
+ upstream.getPushEvent(evalScope).map { evalScope.transform(it) }
+}
+
+internal inline fun <A, B> mapImpl(
+ crossinline upstream: suspend EvalScope.() -> TFlowImpl<A>,
+ noinline transform: suspend EvalScope.(A) -> B,
+): TFlowImpl<B> = TFlowCheap { downstream ->
+ upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
+ ActivationResult(
+ connection =
+ NodeConnection(
+ directUpstream = MapNode(connection.directUpstream, transform),
+ schedulerUpstream = connection.schedulerUpstream,
+ ),
+ needsEval = needsEval,
+ )
+ }
+}
+
+internal class CachedNode<A>(val key: Key<Deferred<Maybe<A>>>, val upstream: PullNode<A>) :
+ PullNode<A> {
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> {
+ val deferred =
+ evalScope.transactionStore.getOrPut(key) {
+ evalScope.deferAsync(CoroutineStart.LAZY) { upstream.getPushEvent(evalScope) }
+ }
+ return deferred.await()
+ }
+}
+
+internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> {
+ val key = object : Key<Deferred<Maybe<A>>> {}
+ return TFlowCheap {
+ activate(this, it)?.let { (connection, needsEval) ->
+ ActivationResult(
+ connection =
+ NodeConnection(
+ directUpstream = CachedNode(key, connection.directUpstream),
+ schedulerUpstream = connection.schedulerUpstream,
+ ),
+ needsEval = needsEval,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt
new file mode 100644
index 0000000..872fb7a
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.kairos.internal
+
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.PriorityBlockingQueue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+internal interface Scheduler {
+ suspend fun schedule(depth: Int, node: MuxNode<*, *, *>)
+
+ suspend fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>)
+}
+
+internal class SchedulerImpl : Scheduler {
+ val enqueued = ConcurrentHashMap<MuxNode<*, *, *>, Any>()
+ val scheduledQ = PriorityBlockingQueue<Pair<Int, MuxNode<*, *, *>>>(16, compareBy { it.first })
+ val chan = Channel<Pair<Int, MuxNode<*, *, *>>>(Channel.UNLIMITED)
+
+ override suspend fun schedule(depth: Int, node: MuxNode<*, *, *>) {
+ if (enqueued.putIfAbsent(node, node) == null) {
+ chan.send(Pair(depth, node))
+ }
+ }
+
+ override suspend fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>) {
+ schedule(Int.MIN_VALUE + indirectDepth, node)
+ }
+
+ suspend fun activate() {
+ for (nodeSchedule in chan) {
+ scheduledQ.add(nodeSchedule)
+ drainChan()
+ }
+ }
+
+ internal suspend fun drainEval(network: Network) {
+ drain { runStep ->
+ runStep { muxNode -> network.evalScope { muxNode.visit(this) } }
+ // If any visited MuxPromptNodes had their depths increased, eagerly propagate those
+ // depth
+ // changes now before performing further network evaluation.
+ network.compactor.drainCompact()
+ }
+ }
+
+ internal suspend fun drainCompact() {
+ drain { runStep -> runStep { muxNode -> muxNode.visitCompact(scheduler = this) } }
+ }
+
+ private suspend inline fun drain(
+ crossinline onStep:
+ suspend (runStep: suspend (visit: suspend (MuxNode<*, *, *>) -> Unit) -> Unit) -> Unit
+ ): Unit = coroutineScope {
+ while (!chan.isEmpty || scheduledQ.isNotEmpty()) {
+ drainChan()
+ val maxDepth = scheduledQ.peek()?.first ?: error("Unexpected empty scheduler")
+ onStep { visit -> runStep(maxDepth, visit) }
+ }
+ }
+
+ private suspend fun drainChan() {
+ while (!chan.isEmpty) {
+ scheduledQ.add(chan.receive())
+ }
+ }
+
+ private suspend inline fun runStep(
+ maxDepth: Int,
+ crossinline visit: suspend (MuxNode<*, *, *>) -> Unit,
+ ) = coroutineScope {
+ while (scheduledQ.peek()?.first?.let { it <= maxDepth } == true) {
+ val (d, node) = scheduledQ.remove()
+ if (
+ node.depthTracker.dirty_hasDirectUpstream() &&
+ d < node.depthTracker.dirty_directDepth
+ ) {
+ scheduledQ.add(node.depthTracker.dirty_directDepth to node)
+ } else {
+ launch {
+ enqueued.remove(node)
+ visit(node)
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000..baf4101
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
@@ -0,0 +1,257 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.FrpDeferredValue
+import com.android.systemui.kairos.FrpStateScope
+import com.android.systemui.kairos.FrpStateful
+import com.android.systemui.kairos.FrpTransactionScope
+import com.android.systemui.kairos.GroupedTFlow
+import com.android.systemui.kairos.TFlow
+import com.android.systemui.kairos.TFlowInit
+import com.android.systemui.kairos.TFlowLoop
+import com.android.systemui.kairos.TState
+import com.android.systemui.kairos.TStateInit
+import com.android.systemui.kairos.emptyTFlow
+import com.android.systemui.kairos.groupByKey
+import com.android.systemui.kairos.init
+import com.android.systemui.kairos.internal.util.mapValuesParallel
+import com.android.systemui.kairos.mapCheap
+import com.android.systemui.kairos.merge
+import com.android.systemui.kairos.switch
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.map
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: TFlow<Any>) :
+ StateScope, EvalScope by evalScope {
+
+ private val endSignalOnce: TFlow<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+
+ private fun <A> TFlow<A>.truncateToScope(operatorName: String): TFlow<A> =
+ if (endSignalOnce === emptyTFlow) {
+ this
+ } else {
+ endSignalOnce.mapCheap { emptyTFlow }.toTStateInternal(operatorName, this).switch()
+ }
+
+ private fun <A> TFlow<A>.nextOnlyInternal(operatorName: String): TFlow<A> =
+ if (this === emptyTFlow) {
+ this
+ } else {
+ TFlowLoop<A>().apply {
+ loopback =
+ mapCheap { emptyTFlow }
+ .toTStateInternal(operatorName, this@nextOnlyInternal)
+ .switch()
+ }
+ }
+
+ private fun <A> TFlow<A>.toTStateInternal(operatorName: String, init: A): TState<A> =
+ toTStateInternalDeferred(operatorName, CompletableDeferred(init))
+
+ private fun <A> TFlow<A>.toTStateInternalDeferred(
+ operatorName: String,
+ init: Deferred<A>,
+ ): TState<A> {
+ val changes = this@toTStateInternalDeferred
+ val name = operatorName
+ val impl =
+ mkState(name, operatorName, evalScope, { changes.init.connect(evalScope = this) }, init)
+ return TStateInit(constInit(name, impl))
+ }
+
+ private fun <R> deferredInternal(block: suspend FrpStateScope.() -> R): FrpDeferredValue<R> =
+ FrpDeferredValue(deferAsync { runInStateScope(block) })
+
+ private fun <A> TFlow<A>.toTStateDeferredInternal(
+ initialValue: FrpDeferredValue<A>
+ ): TState<A> {
+ val operatorName = "toTStateDeferred"
+ // Ensure state is only collected until the end of this scope
+ return truncateToScope(operatorName)
+ .toTStateInternalDeferred(operatorName, initialValue.unwrapped)
+ }
+
+ private fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyInternal(
+ storage: TState<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val name = "mergeIncrementally"
+ return TFlowInit(
+ constInit(
+ name,
+ switchDeferredImpl(
+ getStorage = {
+ storage.init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValuesParallel { (_, flow) -> flow.init.connect(this) }
+ },
+ getPatches = {
+ mapImpl({ init.connect(this) }) { patch ->
+ patch.mapValuesParallel { (_, m) ->
+ m.map { flow -> flow.init.connect(this) }
+ }
+ }
+ },
+ ),
+ )
+ )
+ }
+
+ private fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptInternal(
+ storage: TState<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val name = "mergeIncrementallyPrompt"
+ return TFlowInit(
+ constInit(
+ name,
+ switchPromptImpl(
+ getStorage = {
+ storage.init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValuesParallel { (_, flow) -> flow.init.connect(this) }
+ },
+ getPatches = {
+ mapImpl({ init.connect(this) }) { patch ->
+ patch.mapValuesParallel { (_, m) ->
+ m.map { flow -> flow.init.connect(this) }
+ }
+ }
+ },
+ ),
+ )
+ )
+ }
+
+ private fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKeyInternal(
+ init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> {
+ val eventsByKey: GroupedTFlow<K, Maybe<FrpStateful<A>>> = groupByKey(numKeys)
+ val initOut: Deferred<Map<K, B>> = deferAsync {
+ init.unwrapped.await().mapValuesParallel { (k, stateful) ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childStateScope(newEnd)
+ newScope.runInStateScope(stateful)
+ }
+ }
+ val changesNode: TFlowImpl<Map<K, Maybe<A>>> =
+ mapImpl(
+ upstream = { this@applyLatestStatefulForKeyInternal.init.connect(evalScope = this) }
+ ) { upstreamMap ->
+ upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpStateful<A>>) ->
+ reenterStateScope(this@StateScopeImpl).run {
+ ma.map { stateful ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childStateScope(newEnd)
+ newScope.runInStateScope(stateful)
+ }
+ }
+ }
+ }
+ val operatorName = "applyLatestStatefulForKey"
+ val name = operatorName
+ val changes: TFlow<Map<K, Maybe<A>>> = TFlowInit(constInit(name, changesNode.cached()))
+ return changes to FrpDeferredValue(initOut)
+ }
+
+ private fun <A> TFlow<FrpStateful<A>>.observeStatefulsInternal(): TFlow<A> {
+ val operatorName = "observeStatefuls"
+ val name = operatorName
+ return TFlowInit(
+ constInit(
+ name,
+ mapImpl(
+ upstream = { this@observeStatefulsInternal.init.connect(evalScope = this) }
+ ) { stateful ->
+ reenterStateScope(outerScope = this@StateScopeImpl)
+ .runInStateScope(stateful)
+ }
+ .cached(),
+ )
+ )
+ }
+
+ override val frpScope: FrpStateScope = FrpStateScopeImpl()
+
+ private inner class FrpStateScopeImpl :
+ FrpStateScope, FrpTransactionScope by evalScope.frpScope {
+
+ override fun <A> deferredStateScope(
+ block: suspend FrpStateScope.() -> A
+ ): FrpDeferredValue<A> = deferredInternal(block)
+
+ override fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> =
+ toTStateDeferredInternal(initialValue)
+
+ override fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
+ return mergeIncrementallyInternal(storage)
+ }
+
+ override fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
+ return mergeIncrementallyPromptInternal(storage)
+ }
+
+ override fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestStatefulForKeyInternal(init, numKeys)
+
+ override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> =
+ observeStatefulsInternal()
+ }
+
+ override suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R {
+ val complete = CompletableDeferred<R>(parent = coroutineContext.job)
+ block.startCoroutine(
+ frpScope,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+
+ override fun childStateScope(newEnd: TFlow<Any>) =
+ StateScopeImpl(evalScope, merge(newEnd, endSignal))
+}
+
+private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
+ StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt
new file mode 100644
index 0000000..b904b48
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.util.Maybe
+
+/* Initialized TFlow */
+internal fun interface TFlowImpl<out A> {
+ suspend fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>?
+}
+
+internal data class ActivationResult<out A>(
+ val connection: NodeConnection<A>,
+ val needsEval: Boolean,
+)
+
+internal inline fun <A> TFlowCheap(crossinline cheap: CheapNodeSubscribe<A>) =
+ TFlowImpl { scope, ds ->
+ scope.cheap(ds)
+ }
+
+internal typealias CheapNodeSubscribe<A> =
+ suspend EvalScope.(downstream: Schedulable) -> ActivationResult<A>?
+
+internal data class NodeConnection<out A>(
+ val directUpstream: PullNode<A>,
+ val schedulerUpstream: PushNode<*>,
+)
+
+internal suspend fun <A> NodeConnection<A>.hasCurrentValue(
+ transactionStore: TransactionStore
+): Boolean = schedulerUpstream.hasCurrentValue(transactionStore)
+
+internal suspend fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded(
+ downstream: Schedulable
+) = schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream)
+
+internal suspend fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) =
+ schedulerUpstream.scheduleDeactivationIfNeeded(evalScope)
+
+internal suspend fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) =
+ schedulerUpstream.removeDownstream(downstream)
+
+internal suspend fun <A> NodeConnection<A>.getPushEvent(evalScope: EvalScope): Maybe<A> =
+ directUpstream.getPushEvent(evalScope)
+
+internal val <A> NodeConnection<A>.depthTracker: DepthTracker
+ get() = schedulerUpstream.depthTracker
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt
new file mode 100644
index 0000000..5cec05c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt
@@ -0,0 +1,377 @@
+/*
+ * 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.internal
+
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.internal.util.associateByIndex
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.internal.util.mapValuesParallel
+import com.android.systemui.kairos.util.Just
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.none
+import java.util.concurrent.atomic.AtomicLong
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+internal sealed interface TStateImpl<out A> {
+ val name: String?
+ val operatorName: String
+ val changes: TFlowImpl<A>
+
+ suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long>
+}
+
+internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) :
+ TStateImpl<A>, Key<Deferred<Pair<A, Long>>> {
+
+ @Volatile
+ var invalidatedEpoch = Long.MIN_VALUE
+ private set
+
+ @Volatile
+ protected var cache: Any? = EmptyCache
+ private set
+
+ override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
+ evalScope.transactionStore
+ .getOrPut(this) { evalScope.deferAsync(CoroutineStart.LAZY) { pull(evalScope) } }
+ .await()
+
+ suspend fun pull(evalScope: EvalScope): Pair<A, Long> {
+ @Suppress("UNCHECKED_CAST")
+ return recalc(evalScope)?.also { (a, epoch) -> setCache(a, epoch) }
+ ?: ((cache as A) to invalidatedEpoch)
+ }
+
+ fun setCache(value: A, epoch: Long) {
+ if (epoch > invalidatedEpoch) {
+ cache = value
+ invalidatedEpoch = epoch
+ }
+ }
+
+ fun getCachedUnsafe(): Maybe<A> {
+ @Suppress("UNCHECKED_CAST")
+ return if (cache == EmptyCache) none else just(cache as A)
+ }
+
+ protected abstract suspend fun recalc(evalScope: EvalScope): Pair<A, Long>?
+
+ private data object EmptyCache
+}
+
+internal class TStateSource<A>(
+ override val name: String?,
+ override val operatorName: String,
+ init: Deferred<A>,
+ override val changes: TFlowImpl<A>,
+) : TStateImpl<A> {
+ constructor(
+ name: String?,
+ operatorName: String,
+ init: A,
+ changes: TFlowImpl<A>,
+ ) : this(name, operatorName, CompletableDeferred(init), changes)
+
+ lateinit var upstreamConnection: NodeConnection<A>
+
+ // Note: Don't need to synchronize; we will never interleave reads and writes, since all writes
+ // are performed at the end of a network step, after any reads would have taken place.
+
+ @Volatile private var _current: Deferred<A> = init
+ @Volatile
+ var writeEpoch = 0L
+ private set
+
+ override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
+ _current.await() to writeEpoch
+
+ /** called by network after eval phase has completed */
+ suspend fun updateState(evalScope: EvalScope) {
+ // write the latch
+ val eventResult = upstreamConnection.getPushEvent(evalScope)
+ if (eventResult is Just) {
+ _current = CompletableDeferred(eventResult.value)
+ writeEpoch = evalScope.epoch
+ }
+ }
+
+ override fun toString(): String = "TStateImpl(changes=$changes, current=$_current)"
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getStorageUnsafe(): Maybe<A> =
+ if (_current.isCompleted) just(_current.getCompleted()) else none
+}
+
+internal fun <A> constS(name: String?, operatorName: String, init: A): TStateImpl<A> =
+ TStateSource(name, operatorName, init, neverImpl)
+
+internal inline fun <A> mkState(
+ name: String?,
+ operatorName: String,
+ evalScope: EvalScope,
+ crossinline getChanges: suspend EvalScope.() -> TFlowImpl<A>,
+ init: Deferred<A>,
+): TStateImpl<A> {
+ lateinit var state: TStateSource<A>
+ val calm: TFlowImpl<A> =
+ filterNode(getChanges) { new -> new != state.getCurrentWithEpoch(evalScope = this).first }
+ .cached()
+ return TStateSource(name, operatorName, init, calm).also {
+ state = it
+ evalScope.scheduleOutput(
+ OneShot {
+ calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let {
+ (connection, needsEval) ->
+ state.upstreamConnection = connection
+ if (needsEval) {
+ schedule(state)
+ }
+ }
+ }
+ )
+ }
+}
+
+private inline fun <A> TFlowImpl<A>.calm(
+ crossinline getState: () -> TStateDerived<A>
+): TFlowImpl<A> =
+ filterNode({ this@calm }) { new ->
+ val state = getState()
+ val (current, _) = state.getCurrentWithEpoch(evalScope = this)
+ if (new != current) {
+ state.setCache(new, epoch)
+ true
+ } else {
+ false
+ }
+ }
+ .cached()
+
+internal fun <A, B> TStateImpl<A>.mapCheap(
+ name: String?,
+ operatorName: String,
+ transform: suspend EvalScope.(A) -> B,
+): TStateImpl<B> =
+ DerivedMapCheap(name, operatorName, this, mapImpl({ changes }) { transform(it) }, transform)
+
+internal class DerivedMapCheap<A, B>(
+ override val name: String?,
+ override val operatorName: String,
+ val upstream: TStateImpl<A>,
+ override val changes: TFlowImpl<B>,
+ private val transform: suspend EvalScope.(A) -> B,
+) : TStateImpl<B> {
+
+ override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> {
+ val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
+ return evalScope.transform(a) to epoch
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal fun <A, B> TStateImpl<A>.map(
+ name: String?,
+ operatorName: String,
+ transform: suspend EvalScope.(A) -> B,
+): TStateImpl<B> {
+ lateinit var state: TStateDerived<B>
+ val mappedChanges = mapImpl({ changes }) { transform(it) }.cached().calm { state }
+ state = DerivedMap(name, operatorName, transform, this, mappedChanges)
+ return state
+}
+
+internal class DerivedMap<A, B>(
+ override val name: String?,
+ override val operatorName: String,
+ private val transform: suspend EvalScope.(A) -> B,
+ val upstream: TStateImpl<A>,
+ changes: TFlowImpl<B>,
+) : TStateDerived<B>(changes) {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+
+ override suspend fun recalc(evalScope: EvalScope): Pair<B, Long>? {
+ val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
+ return if (epoch > invalidatedEpoch) {
+ evalScope.transform(a) to epoch
+ } else {
+ null
+ }
+ }
+}
+
+internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: String): TStateImpl<A> {
+ // emits the current value of the new inner state, when that state is emitted
+ val switchEvents = mapImpl({ changes }) { newInner -> newInner.getCurrentWithEpoch(this).first }
+ // emits the new value of the new inner state when that state is emitted, or
+ // falls back to the current value if a new state is *not* being emitted this
+ // transaction
+ val innerChanges =
+ mapImpl({ changes }) { newInner ->
+ mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new }
+ }
+ val switchedChanges: TFlowImpl<A> =
+ mapImpl({
+ switchPromptImpl(
+ getStorage = {
+ mapOf(Unit to this@flatten.getCurrentWithEpoch(evalScope = this).first.changes)
+ },
+ getPatches = { mapImpl({ innerChanges }) { new -> mapOf(Unit to just(new)) } },
+ )
+ }) { map ->
+ map.getValue(Unit)
+ }
+ lateinit var state: DerivedFlatten<A>
+ state = DerivedFlatten(name, operator, this, switchedChanges.calm { state })
+ return state
+}
+
+internal class DerivedFlatten<A>(
+ override val name: String?,
+ override val operatorName: String,
+ val upstream: TStateImpl<TStateImpl<A>>,
+ changes: TFlowImpl<A>,
+) : TStateDerived<A>(changes) {
+ override suspend fun recalc(evalScope: EvalScope): Pair<A, Long> {
+ val (inner, epoch0) = upstream.getCurrentWithEpoch(evalScope)
+ val (a, epoch1) = inner.getCurrentWithEpoch(evalScope)
+ return a to maxOf(epoch0, epoch1)
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <A, B> TStateImpl<A>.flatMap(
+ name: String?,
+ operatorName: String,
+ noinline transform: suspend EvalScope.(A) -> TStateImpl<B>,
+): TStateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName)
+
+internal fun <A, B, Z> zipStates(
+ name: String?,
+ operatorName: String,
+ l1: TStateImpl<A>,
+ l2: TStateImpl<B>,
+ transform: suspend EvalScope.(A, B) -> Z,
+): TStateImpl<Z> =
+ zipStates(null, operatorName, mapOf(0 to l1, 1 to l2)).map(name, operatorName) {
+ val a = it.getValue(0)
+ val b = it.getValue(1)
+ @Suppress("UNCHECKED_CAST") transform(a as A, b as B)
+ }
+
+internal fun <A, B, C, Z> zipStates(
+ name: String?,
+ operatorName: String,
+ l1: TStateImpl<A>,
+ l2: TStateImpl<B>,
+ l3: TStateImpl<C>,
+ transform: suspend EvalScope.(A, B, C) -> Z,
+): TStateImpl<Z> =
+ zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3)).map(name, operatorName) {
+ val a = it.getValue(0)
+ val b = it.getValue(1)
+ val c = it.getValue(2)
+ @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C)
+ }
+
+internal fun <A, B, C, D, Z> zipStates(
+ name: String?,
+ operatorName: String,
+ l1: TStateImpl<A>,
+ l2: TStateImpl<B>,
+ l3: TStateImpl<C>,
+ l4: TStateImpl<D>,
+ transform: suspend EvalScope.(A, B, C, D) -> Z,
+): TStateImpl<Z> =
+ zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3, 3 to l4)).map(
+ name,
+ operatorName,
+ ) {
+ val a = it.getValue(0)
+ val b = it.getValue(1)
+ val c = it.getValue(2)
+ val d = it.getValue(3)
+ @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C, d as D)
+ }
+
+internal fun <K : Any, A> zipStates(
+ name: String?,
+ operatorName: String,
+ states: Map<K, TStateImpl<A>>,
+): TStateImpl<Map<K, A>> {
+ if (states.isEmpty()) return constS(name, operatorName, emptyMap())
+ val stateChanges: Map<K, TFlowImpl<A>> = states.mapValues { it.value.changes }
+ lateinit var state: DerivedZipped<K, A>
+ // No need for calm; invariant ensures that changes will only emit when there's a difference
+ val changes: TFlowImpl<Map<K, A>> =
+ mapImpl({
+ switchDeferredImpl(getStorage = { stateChanges }, getPatches = { neverImpl })
+ }) { patch ->
+ states
+ .mapValues { (k, v) ->
+ if (k in patch) {
+ patch.getValue(k)
+ } else {
+ v.getCurrentWithEpoch(evalScope = this).first
+ }
+ }
+ .also { state.setCache(it, epoch) }
+ }
+ state = DerivedZipped(name, operatorName, states, changes)
+ return state
+}
+
+internal class DerivedZipped<K : Any, A>(
+ override val name: String?,
+ override val operatorName: String,
+ val upstream: Map<K, TStateImpl<A>>,
+ changes: TFlowImpl<Map<K, A>>,
+) : TStateDerived<Map<K, A>>(changes) {
+ override suspend fun recalc(evalScope: EvalScope): Pair<Map<K, A>, Long> {
+ val newEpoch = AtomicLong()
+ return upstream.mapValuesParallel {
+ val (a, epoch) = it.value.getCurrentWithEpoch(evalScope)
+ newEpoch.accumulateAndGet(epoch, ::maxOf)
+ a
+ } to newEpoch.get()
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <A> zipStates(
+ name: String?,
+ operatorName: String,
+ states: List<TStateImpl<A>>,
+): TStateImpl<List<A>> =
+ if (states.isEmpty()) {
+ constS(name, operatorName, emptyList())
+ } else {
+ zipStates(null, operatorName, states.asIterable().associateByIndex()).mapCheap(
+ name,
+ operatorName,
+ ) {
+ it.values.toList()
+ }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt
new file mode 100644
index 0000000..8647bdd
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal
+
+import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.internal.util.hashString
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+
+internal sealed class TransactionalImpl<out A> {
+ data class Const<out A>(val value: Deferred<A>) : TransactionalImpl<A>()
+
+ class Impl<A>(val block: suspend EvalScope.() -> A) : TransactionalImpl<A>(), Key<Deferred<A>> {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <A> transactionalImpl(
+ noinline block: suspend EvalScope.() -> A
+): TransactionalImpl<A> = TransactionalImpl.Impl(block)
+
+internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Deferred<A> =
+ when (this) {
+ is TransactionalImpl.Const -> value
+ is TransactionalImpl.Impl ->
+ evalScope.transactionStore
+ .getOrPut(this) {
+ evalScope.deferAsync(start = CoroutineStart.LAZY) { evalScope.block() }
+ }
+ .also { it.start() }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Bag.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Bag.kt
new file mode 100644
index 0000000..4718519
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Bag.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.internal.util
+
+internal class Bag<T> private constructor(private val intMap: MutableMap<T, Int>) :
+ Set<T> by intMap.keys {
+
+ constructor() : this(hashMapOf())
+
+ override fun toString(): String = intMap.toString()
+
+ fun add(element: T): Boolean {
+ val entry = intMap[element]
+ return if (entry != null) {
+ intMap[element] = entry + 1
+ false
+ } else {
+ intMap[element] = 1
+ true
+ }
+ }
+
+ fun remove(element: T): Boolean {
+ val entry = intMap[element]
+ return when {
+ entry == null -> {
+ false
+ }
+ entry <= 1 -> {
+ intMap.remove(element)
+ true
+ }
+ else -> {
+ intMap[element] = entry - 1
+ false
+ }
+ }
+ }
+
+ fun addAll(elements: Iterable<T>, butNot: T? = null): Set<T>? {
+ val newlyAdded = hashSetOf<T>()
+ for (value in elements) {
+ if (value != butNot) {
+ if (add(value)) {
+ newlyAdded.add(value)
+ }
+ }
+ }
+ return newlyAdded.ifEmpty { null }
+ }
+
+ fun clear() {
+ intMap.clear()
+ }
+
+ fun removeAll(elements: Collection<T>): Set<T>? {
+ val result = hashSetOf<T>()
+ for (element in elements) {
+ if (remove(element)) {
+ result.add(element)
+ }
+ }
+ return result.ifEmpty { null }
+ }
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt
new file mode 100644
index 0000000..6c8ae7c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/ConcurrentNullableHashMap.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal.util
+
+import java.util.concurrent.ConcurrentHashMap
+
+internal class ConcurrentNullableHashMap<K : Any, V>
+private constructor(private val inner: ConcurrentHashMap<K, Any>) {
+ constructor() : this(ConcurrentHashMap())
+
+ @Suppress("UNCHECKED_CAST")
+ operator fun get(key: K): V? = inner[key]?.takeIf { it !== NullValue } as V?
+
+ @Suppress("UNCHECKED_CAST")
+ fun put(key: K, value: V?): V? =
+ inner.put(key, value ?: NullValue)?.takeIf { it !== NullValue } as V?
+
+ operator fun set(key: K, value: V?) {
+ put(key, value)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun toMap(): Map<K, V> = inner.mapValues { (_, v) -> v.takeIf { it !== NullValue } as V }
+
+ fun clear() {
+ inner.clear()
+ }
+
+ fun isNotEmpty(): Boolean = inner.isNotEmpty()
+}
+
+private object NullValue
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
new file mode 100644
index 0000000..5cee2dd
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos.internal.util
+
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.None
+import com.android.systemui.kairos.util.just
+import java.util.concurrent.ConcurrentHashMap
+
+internal interface Key<A>
+
+private object NULL
+
+internal class HeteroMap {
+
+ private val store = ConcurrentHashMap<Key<*>, Any>()
+
+ @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
+
+ operator fun <A> set(key: Key<A>, value: A) {
+ store[key] = value ?: NULL
+ }
+
+ operator fun contains(key: Key<*>): Boolean = store.containsKey(key)
+
+ fun clear() {
+ store.clear()
+ }
+
+ @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
+
+ @Suppress("UNCHECKED_CAST")
+ fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A =
+ store.compute(key) { _, value -> value ?: defaultValue() ?: NULL } as A
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt
new file mode 100644
index 0000000..ebf9a66
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.internal.util
+
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.yield
+
+// TODO: It's possible that this is less efficient than having each coroutine directly insert into a
+// ConcurrentHashMap, but then we would lose ordering
+internal suspend inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>
+ .mapValuesNotNullParallelTo(
+ destination: M,
+ crossinline block: suspend (Map.Entry<K, A>) -> B?,
+): M =
+ destination.also {
+ coroutineScope {
+ mapValues {
+ async {
+ yield()
+ block(it)
+ }
+ }
+ }
+ .mapValuesNotNullTo(it) { (_, deferred) -> deferred.await() }
+ }
+
+internal inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>.mapValuesNotNullTo(
+ destination: M,
+ block: (Map.Entry<K, A>) -> B?,
+): M =
+ destination.also {
+ for (entry in this@mapValuesNotNullTo) {
+ block(entry)?.let { destination.put(entry.key, it) }
+ }
+ }
+
+internal suspend fun <A, B> Iterable<A>.mapParallel(transform: suspend (A) -> B): List<B> =
+ coroutineScope {
+ map { async(start = CoroutineStart.LAZY) { transform(it) } }.awaitAll()
+ }
+
+internal suspend fun <K, A, B, M : MutableMap<K, B>> Map<K, A>.mapValuesParallelTo(
+ destination: M,
+ transform: suspend (Map.Entry<K, A>) -> B,
+): Map<K, B> = entries.mapParallel { it.key to transform(it) }.toMap(destination)
+
+internal suspend fun <K, A, B> Map<K, A>.mapValuesParallel(
+ transform: suspend (Map.Entry<K, A>) -> B
+): Map<K, B> = mapValuesParallelTo(mutableMapOf(), transform)
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
new file mode 100644
index 0000000..6bb7f9f
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.kairos.internal.util
+
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newCoroutineContext
+
+internal fun <A> CoroutineScope.asyncImmediate(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> A,
+): Deferred<A> = async(start = start, context = Dispatchers.Unconfined + context, block = block)
+
+internal fun CoroutineScope.launchImmediate(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit,
+): Job = launch(start = start, context = Dispatchers.Unconfined + context, block = block)
+
+internal suspend fun awaitCancellationAndThen(block: suspend () -> Unit) {
+ try {
+ awaitCancellation()
+ } finally {
+ block()
+ }
+}
+
+internal fun CoroutineScope.launchOnCancel(
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: () -> Unit,
+): Job =
+ launch(context = context, start = CoroutineStart.UNDISPATCHED) {
+ awaitCancellationAndThen(block)
+ }
+
+internal fun CoroutineScope.childScope(
+ context: CoroutineContext = EmptyCoroutineContext
+): CoroutineScope {
+ val newContext = newCoroutineContext(context)
+ val newJob = Job(parent = newContext[Job])
+ return CoroutineScope(newContext + newJob)
+}
+
+internal fun <A> Iterable<A>.associateByIndex(): Map<Int, A> = buildMap {
+ forEachIndexed { index, a -> put(index, a) }
+}
+
+internal fun <A, M : MutableMap<Int, A>> Iterable<A>.associateByIndexTo(destination: M): M =
+ destination.apply { forEachIndexed { index, a -> put(index, a) } }
+
+internal val Any.hashString: String
+ get() = Integer.toHexString(System.identityHashCode(this))
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
new file mode 100644
index 0000000..ad9f7d7
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package com.android.systemui.kairos.util
+
+/**
+ * Contains a value of two possibilities: `Left<A>` or `Right<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 class Either<out A, out B>
+
+/** An [Either] that contains a [Left] value. */
+data class Left<out A>(val value: A) : Either<A, Nothing>()
+
+/** An [Either] that contains a [Right] value. */
+data class Right<out B>(val value: B) : Either<Nothing, B>()
+
+/**
+ * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the
+ * [Right] value unchanged.
+ */
+inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> =
+ when (this) {
+ is Left -> Left(transform(value))
+ is Right -> this
+ }
+
+/**
+ * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the
+ * [Left] value unchanged.
+ */
+inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> =
+ when (this) {
+ is Left -> this
+ is Right -> Right(transform(value))
+ }
+
+/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */
+inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> =
+ when (this) {
+ is Left -> just(value)
+ else -> None
+ }
+
+/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */
+inline fun <A> Either<A, *>.leftOrNull(): A? =
+ when (this) {
+ is Left -> value
+ else -> null
+ }
+
+/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */
+inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> =
+ when (this) {
+ is Right -> just(value)
+ else -> None
+ }
+
+/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */
+inline fun <B> Either<*, B>.rightOrNull(): B? =
+ when (this) {
+ is Right -> value
+ else -> null
+ }
+
+/**
+ * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and
+ * [Pair.second] contains all [Right] values.
+ */
+fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> {
+ val lefts = mutableListOf<A>()
+ val rights = mutableListOf<B>()
+ for (either in this) {
+ when (either) {
+ is Left -> lefts.add(either.value)
+ is Right -> rights.add(either.value)
+ }
+ }
+ return lefts to rights
+}
+
+/**
+ * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values,
+ * and [Pair.second] contains all [Right] 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>()
+ for ((k, e) in this) {
+ when (e) {
+ is Left -> lefts[k] = e.value
+ is Right -> rights[k] = e.value
+ }
+ }
+ return lefts to rights
+}
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
new file mode 100644
index 0000000..c3cae38
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "SuspendCoroutine")
+
+package com.android.systemui.kairos.util
+
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.RestrictsSuspension
+import kotlin.coroutines.resume
+import kotlin.coroutines.startCoroutine
+import kotlin.coroutines.suspendCoroutine
+
+/** Represents a value that may or may not be present. */
+sealed class Maybe<out A>
+
+/** A [Maybe] value that is present. */
+data class Just<out A> internal constructor(val value: A) : Maybe<A>()
+
+/** A [Maybe] value that is not present. */
+data object None : Maybe<Nothing>()
+
+/** 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)
+ }
+
+ suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k ->
+ if (block()) k.resume(Unit)
+ }
+}
+
+/**
+ * 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.
+ *
+ * This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing
+ * with complex combinations of nullables:
+ * ``` kotlin
+ * val aMaybe: Maybe<Any> = ...
+ * val bMaybe: Maybe<Any> = ...
+ * val result: String = maybe {
+ * val a = !aMaybe
+ * val b = !bMaybe
+ * "Got: $a and $b"
+ * }
+ * ```
+ */
+fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> {
+ var maybeResult: Maybe<A> = None
+ val k =
+ object : Continuation<A> {
+ override val context: CoroutineContext = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<A>) {
+ maybeResult = result.getOrNull()?.let { just(it) } ?: None
+ }
+ }
+ block.startCoroutine(MaybeScope, k)
+ return maybeResult
+}
+
+/** Returns a [Just] containing this value, or [None] if `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 [Just] containing [value]. */
+fun <A> just(value: A): Maybe<A> = Just(value)
+
+/** 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 the value present in this [Maybe], or `null` if not present. */
+inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
+
+/**
+ * Returns a [Maybe] holding the result of applying [transform] to the value in the original
+ * [Maybe].
+ */
+inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> =
+ when (this) {
+ is Just -> just(transform(value))
+ is None -> None
+ }
+
+/** 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
+ }
+
+/** 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()
+ }
+
+/**
+ * Returns the value present in this [Maybe], or invokes [error] with the message returned from
+ * [getMessage].
+ */
+inline fun <A> Maybe<A>.orError(getMessage: () -> Any): A = orElseGet { error(getMessage()) }
+
+/** 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
+ }
+
+/**
+ * Returns a [Maybe] that contains the present in the original [Maybe], only if it satisfies
+ * [predicate].
+ */
+inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> =
+ when (this) {
+ is Just -> if (predicate(value)) this else None
+ 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()
+
+/** 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 }
+
+// Align
+
+/**
+ * Returns a [Maybe] containing the result of applying the values present in the original [Maybe]
+ * and other, applied to [transform] as a [These].
+ */
+inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> =
+ when (this) {
+ is Just -> {
+ val a = value
+ when (other) {
+ is Just -> {
+ val b = other.value
+ just(transform(These.both(a, b)))
+ }
+ None -> just(transform(These.thiz(a)))
+ }
+ }
+ None ->
+ when (other) {
+ is Just -> {
+ val b = other.value
+ just(transform(These.that(b)))
+ }
+ None -> none
+ }
+ }
+
+// Alt
+
+/** Returns a [Maybe] containing the value present in the original [Maybe], or [other]. */
+infix fun <A> Maybe<A>.orElseMaybe(other: Maybe<A>): Maybe<A> = orElseGetMaybe { other }
+
+/**
+ * Returns a [Maybe] containing the value present in the original [Maybe], or the result of [other].
+ */
+inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> =
+ when (this) {
+ is Just -> this
+ else -> other()
+ }
+
+// Apply
+
+/**
+ * Returns a [Maybe] containing the value present in [argMaybe] applied to the function present in
+ * the original [Maybe].
+ */
+fun <A, B> Maybe<(A) -> B>.apply(argMaybe: Maybe<A>): Maybe<B> = flatMap { f ->
+ argMaybe.map { a -> f(a) }
+}
+
+/**
+ * Returns a [Maybe] containing the result of applying [transform] to the values present in the
+ * original [Maybe] and [other].
+ */
+inline fun <A, B, C> Maybe<A>.zipWith(other: Maybe<B>, transform: (A, B) -> C) = flatMap { a ->
+ other.map { b -> transform(a, b) }
+}
+
+// Bind
+
+/**
+ * Returns a [Maybe] containing the value present in the [Maybe] present in the original [Maybe].
+ */
+fun <A> Maybe<Maybe<A>>.flatten(): Maybe<A> = flatMap { it }
+
+// Semigroup
+
+/**
+ * Returns a [Maybe] containing the result of applying the values present in the original [Maybe]
+ * and other, applied to [transform].
+ */
+fun <A> Maybe<A>.mergeWith(other: Maybe<A>, transform: (A, A) -> A): Maybe<A> =
+ alignWith(other) { it.merge(transform) }
+
+/**
+ * Returns a list containing only the present results of applying [transform] to each element in the
+ * original iterable.
+ */
+fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> =
+ asSequence().mapMaybe(transform).toList()
+
+/**
+ * Returns a sequence containing only the present results of applying [transform] to each element in
+ * the original sequence.
+ */
+fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> =
+ map(transform).filterIsInstance<Just<B>>().map { it.value }
+
+/**
+ * Returns a map with values of only the present results of applying [transform] to each entry in
+ * the original map.
+ */
+inline fun <K, A, B> Map<K, A>.mapMaybeValues(
+ crossinline p: (Map.Entry<K, A>) -> Maybe<B>
+): Map<K, B> = asSequence().mapMaybe { entry -> p(entry).map { entry.key to it } }.toMap()
+
+/** Returns a map with all non-present values filtered out. */
+fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> =
+ asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap()
+
+/**
+ * Returns a pair of [Maybes][Maybe] that contain the [Pair.first] and [Pair.second] values present
+ * in the original [Maybe].
+ */
+fun <A, B> Maybe<Pair<A, B>>.splitPair(): Pair<Maybe<A>, Maybe<B>> =
+ map { it.first } to map { it.second }
+
+/** Returns the value associated with [key] in this map as a [Maybe]. */
+fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> {
+ val value = get(key)
+ if (value == null && !containsKey(key)) {
+ return none
+ } else {
+ @Suppress("UNCHECKED_CAST")
+ return just(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
new file mode 100644
index 0000000..aa95e0d
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.util
+
+/** Contains at least one of two potential values. */
+sealed class These<A, B> {
+ /** Contains a single potential value. */
+ class This<A, B> internal constructor(val thiz: A) : These<A, B>()
+
+ /** Contains a single potential value. */
+ class That<A, B> internal constructor(val that: B) : These<A, B>()
+
+ /** Contains both potential values. */
+ class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>()
+
+ companion object {
+ /** Constructs a [These] containing only [thiz]. */
+ fun <A, B> thiz(thiz: A): These<A, B> = This(thiz)
+
+ /** Constructs a [These] containing only [that]. */
+ fun <A, B> that(that: B): These<A, B> = That(that)
+
+ /** Constructs a [These] containing both [thiz] and [that]. */
+ fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that)
+ }
+}
+
+/**
+ * Returns a single value from this [These]; either the single value held within, or the result of
+ * applying [f] to both values.
+ */
+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)
+ }
+
+/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */
+fun <A> These<A, *>.maybeThis(): Maybe<A> =
+ when (this) {
+ is These.Both -> just(thiz)
+ is These.That -> None
+ is These.This -> just(thiz)
+ }
+
+/**
+ * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not
+ * present.
+ */
+fun <A : Any> These<A, *>.thisOrNull(): A? =
+ when (this) {
+ is These.Both -> thiz
+ is These.That -> null
+ is These.This -> thiz
+ }
+
+/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */
+fun <A> These<*, A>.maybeThat(): Maybe<A> =
+ when (this) {
+ is These.Both -> just(that)
+ is These.That -> just(that)
+ is These.This -> None
+ }
+
+/**
+ * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not
+ * present.
+ */
+fun <A : Any> These<*, A>.thatOrNull(): A? =
+ when (this) {
+ is These.Both -> that
+ is These.That -> that
+ is These.This -> 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
+ }
+
+/** 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)
+ }
+ )
+ else ->
+ when (that) {
+ is Just -> just(These.that(that.value))
+ else -> none
+ }
+ }
+
+/**
+ * Returns a [These] containing [thiz] and/or [that] 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) }
+
+/**
+ * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second]
+ * containing all [These.That] 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.
+ */
+fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
+ val a = mutableMapOf<K, A>()
+ val b = mutableMapOf<K, B>()
+ for ((k, t) in this) {
+ when (t) {
+ is These.Both -> {
+ a[k] = t.thiz
+ b[k] = t.that
+ }
+ is These.That -> {
+ b[k] = t.that
+ }
+ is These.This -> {
+ a[k] = t.thiz
+ }
+ }
+ }
+ return a to b
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt
new file mode 100644
index 0000000..5cfaa3e
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/WithPrev.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.util
+
+/** Holds a [newValue] emitted from a `TFlow`, along with the [previousValue] emitted value. */
+data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
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
new file mode 100644
index 0000000..165230b
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
@@ -0,0 +1,1370 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFrpApi::class)
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.None
+import com.android.systemui.kairos.util.Right
+import com.android.systemui.kairos.util.just
+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
+import kotlin.time.measureTime
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class KairosTests {
+
+ @Test
+ fun basic() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ var result: Int? = null
+ activateSpec(network) { emitter.observe { result = it } }
+ runCurrent()
+ emitter.emit(3)
+ runCurrent()
+ assertEquals(3, result)
+ runCurrent()
+ }
+
+ @Test
+ fun basicTFlow() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ println("starting network")
+ val result = activateSpecWithResult(network) { emitter.nextDeferred() }
+ runCurrent()
+ println("emitting")
+ emitter.emit(3)
+ runCurrent()
+ println("awaiting")
+ assertEquals(3, result.await())
+ runCurrent()
+ }
+
+ @Test
+ fun basicTState() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ val result = activateSpecWithResult(network) { emitter.hold(0).stateChanges.nextDeferred() }
+ runCurrent()
+
+ emitter.emit(3)
+ runCurrent()
+
+ assertEquals(3, result.await())
+ }
+
+ @Test
+ fun basicEvent() = runFrpTest { network ->
+ val emitter = MutableSharedFlow<Int>()
+ val result = activateSpecWithResult(network) { async { emitter.first() } }
+ runCurrent()
+ emitter.emit(1)
+ runCurrent()
+ assertTrue("Result eventual has not completed.", result.isCompleted)
+ assertEquals(1, result.await())
+ }
+
+ @Test
+ fun basicTransactional() = runFrpTest { network ->
+ var value: Int? = null
+ var bSource = 1
+ val emitter = network.mutableTFlow<Unit>()
+ // Sampling this transactional will increment the source count.
+ val transactional = transactionally { bSource++ }
+ measureTime {
+ activateSpecWithResult(network) {
+ // Two different flows that sample the same transactional.
+ (0 until 2).map {
+ val sampled = emitter.sample(transactional) { _, v -> v }
+ sampled.toSharedFlow()
+ }
+ }
+ .forEach { backgroundScope.launch { it.collect { value = it } } }
+ runCurrent()
+ }
+ .also { println("setup: ${it.toString(DurationUnit.MILLISECONDS, 2)}") }
+
+ measureTime {
+ emitter.emit(Unit)
+ runCurrent()
+ }
+ .also { println("emit 1: ${it.toString(DurationUnit.MILLISECONDS, 2)}") }
+
+ // Even though the transactional would be sampled twice, the first result is cached.
+ assertEquals(2, bSource)
+ assertEquals(1, value)
+
+ measureTime {
+ bSource = 10
+ emitter.emit(Unit)
+ runCurrent()
+ }
+ .also { println("emit 2: ${it.toString(DurationUnit.MILLISECONDS, 2)}") }
+
+ assertEquals(11, bSource)
+ assertEquals(10, value)
+ }
+
+ @Test
+ fun diamondGraph() = runFrpTest { network ->
+ val flow = network.mutableTFlow<Int>()
+ val outFlow =
+ activateSpecWithResult(network) {
+ // map TFlow like we map Flow
+ val left = flow.map { "left" to it }.onEach { println("left: $it") }
+ val right = flow.map { "right" to it }.onEach { println("right: $it") }
+
+ // convert TFlows to TStates so that they can be combined
+ val combined =
+ left.hold("left" to 0).combineWith(right.hold("right" to 0)) { l, r -> l to r }
+ combined.stateChanges // get TState changes
+ .onEach { println("merged: $it") }
+ .toSharedFlow() // convert back to Flow
+ }
+ runCurrent()
+
+ val results = mutableListOf<Pair<Pair<String, Int>, Pair<String, Int>>>()
+ backgroundScope.launch { outFlow.toCollection(results) }
+ runCurrent()
+
+ flow.emit(1)
+ runCurrent()
+
+ flow.emit(2)
+ runCurrent()
+
+ assertEquals(
+ listOf(("left" to 1) to ("right" to 1), ("left" to 2) to ("right" to 2)),
+ results,
+ )
+ }
+
+ @Test
+ fun staticNetwork() = runFrpTest { network ->
+ var finalSum: Int? = null
+
+ val intEmitter = network.mutableTFlow<Int>()
+ val sampleEmitter = network.mutableTFlow<Unit>()
+
+ activateSpecWithResult(network) {
+ val updates = intEmitter.map { a -> { b: Int -> a + b } }
+
+ val sumD =
+ TStateLoop<Int>().apply {
+ loopback =
+ updates
+ .sample(this) { f, sum -> f(sum) }
+ .onEach { println("sum update: $it") }
+ .hold(0)
+ }
+ sampleEmitter
+ .onEach { println("sampleEmitter emitted") }
+ .sample(sumD) { _, sum -> sum }
+ .onEach { println("sampled: $it") }
+ .nextDeferred()
+ }
+ .let { launch { finalSum = it.await() } }
+
+ runCurrent()
+
+ (1..5).forEach { i ->
+ println("emitting: $i")
+ intEmitter.emit(i)
+ runCurrent()
+ }
+ runCurrent()
+
+ sampleEmitter.emit(Unit)
+ runCurrent()
+
+ assertEquals(15, finalSum)
+ }
+
+ @Test
+ fun recursiveDefinition() = runFrpTest { network ->
+ var wasSold = false
+ var currentAmt: Int? = null
+
+ val coin = network.mutableTFlow<Unit>()
+ val price = 50
+ val frpSpec = frpSpec {
+ val eSold = TFlowLoop<Unit>()
+
+ val eInsert =
+ coin.map {
+ { runningTotal: Int ->
+ println("TEST: $runningTotal - 10 = ${runningTotal - 10}")
+ runningTotal - 10
+ }
+ }
+
+ val eReset =
+ eSold.map {
+ { _: Int ->
+ println("TEST: Resetting")
+ price
+ }
+ }
+
+ val eUpdate = eInsert.mergeWith(eReset) { f, g -> { a -> g(f(a)) } }
+
+ val dTotal = TStateLoop<Int>()
+ dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.hold(price)
+
+ val eAmt = dTotal.stateChanges
+ val bAmt = transactionally { dTotal.sample() }
+ eSold.loopback =
+ coin
+ .sample(bAmt) { coin, total -> coin to total }
+ .mapMaybe { (_, total) -> maybe { guard { total <= 10 } } }
+
+ val amts = eAmt.filter { amt -> amt >= 0 }
+
+ amts.observe { currentAmt = it }
+ eSold.observe { wasSold = true }
+
+ eSold.nextDeferred()
+ }
+
+ activateSpec(network) { frpSpec.applySpec() }
+
+ runCurrent()
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(40, currentAmt)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(30, currentAmt)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(20, currentAmt)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(10, currentAmt)
+ assertEquals(false, wasSold)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(true, wasSold)
+ assertEquals(50, currentAmt)
+ }
+
+ @Test
+ fun promptCleanup() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ val stopper = network.mutableTFlow<Unit>()
+
+ var result: Int? = null
+
+ val flow = activateSpecWithResult(network) { emitter.takeUntil(stopper).toSharedFlow() }
+ backgroundScope.launch { flow.collect { result = it } }
+ runCurrent()
+
+ emitter.emit(2)
+ runCurrent()
+
+ assertEquals(2, result)
+
+ stopper.emit(Unit)
+ runCurrent()
+ }
+
+ @Test
+ fun switchTFlow() = runFrpTest { network ->
+ var currentSum: Int? = null
+
+ val switchHandler = network.mutableTFlow<Pair<TFlow<Int>, String>>()
+ val aHandler = network.mutableTFlow<Int>()
+ val stopHandler = network.mutableTFlow<Unit>()
+ val bHandler = network.mutableTFlow<Int>()
+
+ val sumFlow =
+ activateSpecWithResult(network) {
+ val switchE = TFlowLoop<TFlow<Int>>()
+ switchE.loopback =
+ switchHandler.mapStateful { (intFlow, name) ->
+ println("[onEach] Switching to: $name")
+ val nextSwitch =
+ switchE.skipNext().onEach { println("[onEach] switched-out") }
+ val stopEvent =
+ stopHandler
+ .onEach { println("[onEach] stopped") }
+ .mergeWith(nextSwitch) { _, b -> b }
+ intFlow.takeUntil(stopEvent)
+ }
+
+ val adderE: TFlow<(Int) -> Int> =
+ switchE.hold(emptyTFlow).switch().map { a ->
+ println("[onEach] new number $a")
+ ({ sum: Int ->
+ println("$a+$sum=${a + sum}")
+ sum + a
+ })
+ }
+
+ val sumD = TStateLoop<Int>()
+ sumD.loopback =
+ adderE
+ .sample(sumD) { f, sum -> f(sum) }
+ .onEach { println("[onEach] writing sum: $it") }
+ .hold(0)
+ val sumE = sumD.stateChanges
+
+ sumE.toSharedFlow()
+ }
+
+ runCurrent()
+
+ backgroundScope.launch { sumFlow.collect { currentSum = it } }
+
+ runCurrent()
+
+ switchHandler.emit(aHandler to "A")
+ runCurrent()
+
+ aHandler.emit(1)
+ runCurrent()
+
+ assertEquals(1, currentSum)
+
+ aHandler.emit(2)
+ runCurrent()
+
+ assertEquals(3, currentSum)
+
+ aHandler.emit(3)
+ runCurrent()
+
+ assertEquals(6, currentSum)
+
+ aHandler.emit(4)
+ runCurrent()
+
+ assertEquals(10, currentSum)
+
+ aHandler.emit(5)
+ runCurrent()
+
+ assertEquals(15, currentSum)
+
+ switchHandler.emit(bHandler to "B")
+ runCurrent()
+
+ aHandler.emit(6)
+ runCurrent()
+
+ assertEquals(15, currentSum)
+
+ bHandler.emit(6)
+ runCurrent()
+
+ assertEquals(21, currentSum)
+
+ bHandler.emit(7)
+ runCurrent()
+
+ assertEquals(28, currentSum)
+
+ bHandler.emit(8)
+ runCurrent()
+
+ assertEquals(36, currentSum)
+
+ bHandler.emit(9)
+ runCurrent()
+
+ assertEquals(45, currentSum)
+
+ bHandler.emit(10)
+ runCurrent()
+
+ assertEquals(55, currentSum)
+
+ println()
+ println("Stopping: B")
+ stopHandler.emit(Unit) // bHandler.complete()
+ runCurrent()
+
+ bHandler.emit(20)
+ runCurrent()
+
+ assertEquals(55, currentSum)
+
+ println()
+ println("Switching to: A2")
+ switchHandler.emit(aHandler to "A2")
+ runCurrent()
+
+ println("aHandler.emit(11)")
+ aHandler.emit(11)
+ runCurrent()
+
+ assertEquals(66, currentSum)
+
+ aHandler.emit(12)
+ runCurrent()
+
+ assertEquals(78, currentSum)
+
+ aHandler.emit(13)
+ runCurrent()
+
+ assertEquals(91, currentSum)
+
+ aHandler.emit(14)
+ runCurrent()
+
+ assertEquals(105, currentSum)
+
+ aHandler.emit(15)
+ runCurrent()
+
+ assertEquals(120, currentSum)
+
+ stopHandler.emit(Unit)
+ runCurrent()
+
+ aHandler.emit(100)
+ runCurrent()
+
+ assertEquals(120, currentSum)
+ }
+
+ @Test
+ fun switchIndirect() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ activateSpec(network) {
+ emptyTFlow.map { emitter.map { 1 } }.flatten().map { "$it" }.observe()
+ }
+ runCurrent()
+ }
+
+ @Test
+ fun switchInWithResult() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ val out =
+ activateSpecWithResult(network) {
+ emitter.map { emitter.map { 1 } }.flatten().toSharedFlow()
+ }
+ val result = out.stateIn(backgroundScope, SharingStarted.Eagerly, null)
+ runCurrent()
+ emitter.emit(Unit)
+ runCurrent()
+ assertEquals(null, result.value)
+ }
+
+ @Test
+ fun switchInCompleted() = runFrpTest { network ->
+ val outputs = mutableListOf<Int>()
+
+ val switchAH = network.mutableTFlow<Unit>()
+ val intAH = network.mutableTFlow<Int>()
+ val stopEmitter = network.mutableTFlow<Unit>()
+
+ val top = frpSpec {
+ val intS = intAH.takeUntil(stopEmitter)
+ val switched = switchAH.map { intS }.flatten()
+ switched.toSharedFlow()
+ }
+ val flow = activateSpecWithResult(network) { top.applySpec() }
+ backgroundScope.launch { flow.collect { outputs.add(it) } }
+ runCurrent()
+
+ switchAH.emit(Unit)
+ runCurrent()
+
+ stopEmitter.emit(Unit)
+ runCurrent()
+
+ // assertEquals(0, intAH.subscriptionCount.value)
+ intAH.emit(10)
+ runCurrent()
+
+ assertEquals(true, outputs.isEmpty())
+
+ switchAH.emit(Unit)
+ runCurrent()
+
+ // assertEquals(0, intAH.subscriptionCount.value)
+ intAH.emit(10)
+ runCurrent()
+
+ assertEquals(true, outputs.isEmpty())
+ }
+
+ @Test
+ fun switchTFlow_outerCompletesFirst() = runFrpTest { network ->
+ var stepResult: Int? = null
+
+ val switchAH = network.mutableTFlow<Unit>()
+ val switchStopEmitter = network.mutableTFlow<Unit>()
+ val intStopEmitter = network.mutableTFlow<Unit>()
+ val intAH = network.mutableTFlow<Int>()
+ val flow =
+ activateSpecWithResult(network) {
+ val intS = intAH.takeUntil(intStopEmitter)
+ val switchS = switchAH.takeUntil(switchStopEmitter)
+
+ val switched = switchS.map { intS }.flatten()
+ switched.toSharedFlow()
+ }
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ // assertEquals(0, intAH.subscriptionCount.value)
+ intAH.emit(100)
+ runCurrent()
+
+ assertEquals(null, stepResult)
+
+ switchAH.emit(Unit)
+ runCurrent()
+
+ // assertEquals(1, intAH.subscriptionCount.value)
+
+ intAH.emit(5)
+ runCurrent()
+
+ assertEquals(5, stepResult)
+
+ println("stop outer")
+ switchStopEmitter.emit(Unit) // switchAH.complete()
+ runCurrent()
+
+ // assertEquals(1, intAH.subscriptionCount.value)
+ // assertEquals(0, switchAH.subscriptionCount.value)
+
+ intAH.emit(10)
+ runCurrent()
+
+ assertEquals(10, stepResult)
+
+ println("stop inner")
+ intStopEmitter.emit(Unit) // intAH.complete()
+ runCurrent()
+
+ // assertEquals(just(10), network.await())
+ }
+
+ @Test
+ fun mapTFlow() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ var stepResult: Int? = null
+
+ val flow =
+ activateSpecWithResult(network) {
+ val mappedS = emitter.map { it * it }
+ mappedS.toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ emitter.emit(1)
+ runCurrent()
+
+ assertEquals(1, stepResult)
+
+ emitter.emit(2)
+ runCurrent()
+
+ assertEquals(4, stepResult)
+
+ emitter.emit(10)
+ runCurrent()
+
+ assertEquals(100, stepResult)
+ }
+
+ @Test
+ fun mapTransactional() = runFrpTest { network ->
+ var doubledResult: Int? = null
+ var pullValue = 0
+ val a = transactionally { pullValue }
+ val b = transactionally { a.sample() * 2 }
+ val emitter = network.mutableTFlow<Unit>()
+ val flow =
+ activateSpecWithResult(network) {
+ val sampleB = emitter.sample(b) { _, b -> b }
+ sampleB.toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { doubledResult = it } }
+
+ runCurrent()
+
+ emitter.emit(Unit)
+ runCurrent()
+
+ assertEquals(0, doubledResult)
+
+ pullValue = 5
+ emitter.emit(Unit)
+ runCurrent()
+
+ assertEquals(10, doubledResult)
+ }
+
+ @Test
+ fun mapTState() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ var stepResult: Int? = null
+ val flow =
+ activateSpecWithResult(network) {
+ val state = emitter.hold(0).map { it + 2 }
+ val stateCurrent = transactionally { state.sample() }
+ val stateChanges = state.stateChanges
+ val sampleState = emitter.sample(stateCurrent) { _, b -> b }
+ val merge = stateChanges.mergeWith(sampleState) { a, b -> a + b }
+ merge.toSharedFlow()
+ }
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ emitter.emit(1)
+ runCurrent()
+
+ assertEquals(5, stepResult)
+
+ emitter.emit(10)
+ runCurrent()
+
+ assertEquals(15, stepResult)
+ }
+
+ @Test
+ fun partitionEither() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Either<Int, Int>>()
+ val result =
+ activateSpecWithResult(network) {
+ val (l, r) = emitter.partitionEither()
+ val pDiamond =
+ l.map { it * 2 }
+ .mergeWith(r.map { it * -1 }) { _, _ -> error("unexpected coincidence") }
+ pDiamond.hold(null).toStateFlow()
+ }
+ runCurrent()
+
+ emitter.emit(Left(10))
+ runCurrent()
+
+ assertEquals(20, result.value)
+
+ emitter.emit(Right(30))
+ runCurrent()
+
+ assertEquals(-30, result.value)
+ }
+
+ @Test
+ fun accumTState() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ val sampler = network.mutableTFlow<Unit>()
+ var stepResult: Int? = null
+ val flow =
+ activateSpecWithResult(network) {
+ val sumState = emitter.map { a -> { b: Int -> a + b } }.fold(0) { f, a -> f(a) }
+
+ sumState.stateChanges
+ .mergeWith(sampler.sample(sumState) { _, sum -> sum }) { _, _ ->
+ error("Unexpected coincidence")
+ }
+ .toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ emitter.emit(5)
+ runCurrent()
+ assertEquals(5, stepResult)
+
+ emitter.emit(10)
+ runCurrent()
+ assertEquals(15, stepResult)
+
+ sampler.emit(Unit)
+ runCurrent()
+ assertEquals(15, stepResult)
+ }
+
+ @Test
+ fun mergeTFlows() = runFrpTest { network ->
+ val first = network.mutableTFlow<Int>()
+ val stopFirst = network.mutableTFlow<Unit>()
+ val second = network.mutableTFlow<Int>()
+ val stopSecond = network.mutableTFlow<Unit>()
+ var stepResult: Int? = null
+
+ val flow: SharedFlow<Int>
+ val setupDuration = measureTime {
+ flow =
+ activateSpecWithResult(network) {
+ val firstS = first.takeUntil(stopFirst)
+ val secondS = second.takeUntil(stopSecond)
+ val mergedS =
+ firstS.mergeWith(secondS) { _, _ -> error("Unexpected coincidence") }
+ mergedS.toSharedFlow()
+ // mergedS.last("onComplete")
+ }
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+ }
+
+ // assertEquals(1, first.subscriptionCount.value)
+ // assertEquals(1, second.subscriptionCount.value)
+
+ val firstEmitDuration = measureTime {
+ first.emit(1)
+ runCurrent()
+ }
+
+ assertEquals(1, stepResult)
+
+ val secondEmitDuration = measureTime {
+ second.emit(2)
+ runCurrent()
+ }
+
+ assertEquals(2, stepResult)
+
+ val stopFirstDuration = measureTime {
+ stopFirst.emit(Unit)
+ runCurrent()
+ }
+
+ // assertEquals(0, first.subscriptionCount.value)
+ val testDeadEmitFirstDuration = measureTime {
+ first.emit(10)
+ runCurrent()
+ }
+
+ assertEquals(2, stepResult)
+
+ // assertEquals(1, second.subscriptionCount.value)
+
+ val secondEmitDuration2 = measureTime {
+ second.emit(3)
+ runCurrent()
+ }
+
+ assertEquals(3, stepResult)
+
+ val stopSecondDuration = measureTime {
+ stopSecond.emit(Unit)
+ runCurrent()
+ }
+
+ // assertEquals(0, second.subscriptionCount.value)
+ val testDeadEmitSecondDuration = measureTime {
+ second.emit(10)
+ runCurrent()
+ }
+
+ assertEquals(3, stepResult)
+
+ println(
+ """
+ setupDuration: ${setupDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ firstEmitDuration: ${firstEmitDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ secondEmitDuration: ${secondEmitDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ stopFirstDuration: ${stopFirstDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ testDeadEmitFirstDuration: ${
+ testDeadEmitFirstDuration.toString(
+ DurationUnit.MILLISECONDS,
+ 2,
+ )
+ }
+ secondEmitDuration2: ${secondEmitDuration2.toString(DurationUnit.MILLISECONDS, 2)}
+ stopSecondDuration: ${stopSecondDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ testDeadEmitSecondDuration: ${
+ testDeadEmitSecondDuration.toString(
+ DurationUnit.MILLISECONDS,
+ 2,
+ )
+ }
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun sampleCancel() = runFrpTest { network ->
+ val updater = network.mutableTFlow<Int>()
+ val stopUpdater = network.mutableTFlow<Unit>()
+ val sampler = network.mutableTFlow<Unit>()
+ val stopSampler = network.mutableTFlow<Unit>()
+ var stepResult: Int? = null
+ val flow =
+ activateSpecWithResult(network) {
+ val stopSamplerFirst = stopSampler
+ val samplerS = sampler.takeUntil(stopSamplerFirst)
+ val stopUpdaterFirst = stopUpdater
+ val updaterS = updater.takeUntil(stopUpdaterFirst)
+ val sampledS = samplerS.sample(updaterS.hold(0)) { _, b -> b }
+ sampledS.toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ updater.emit(1)
+ runCurrent()
+
+ sampler.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult)
+
+ stopSampler.emit(Unit)
+ runCurrent()
+
+ // assertEquals(0, updater.subscriptionCount.value)
+ // assertEquals(0, sampler.subscriptionCount.value)
+ updater.emit(10)
+ runCurrent()
+
+ sampler.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult)
+ }
+
+ @Test
+ fun combineStates_differentUpstreams() = runFrpTest { network ->
+ val a = network.mutableTFlow<Int>()
+ val b = network.mutableTFlow<Int>()
+ var observed: Pair<Int, Int>? = null
+ val tState =
+ activateSpecWithResult(network) {
+ val state = combine(a.hold(0), b.hold(0)) { a, b -> Pair(a, b) }
+ state.stateChanges.observe { observed = it }
+ state
+ }
+ assertEquals(0 to 0, network.transact { tState.sample() })
+ assertEquals(null, observed)
+ a.emit(5)
+ assertEquals(5 to 0, observed)
+ assertEquals(5 to 0, network.transact { tState.sample() })
+ b.emit(3)
+ assertEquals(5 to 3, observed)
+ assertEquals(5 to 3, network.transact { tState.sample() })
+ }
+
+ @Test
+ fun sampleCombinedStates() = runFrpTest { network ->
+ val updater = network.mutableTFlow<Int>()
+ val emitter = network.mutableTFlow<Unit>()
+
+ val result =
+ activateSpecWithResult(network) {
+ val bA = updater.map { it * 2 }.hold(0)
+ val bB = updater.hold(0)
+ val combineD: TState<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b }
+ val sampleS = emitter.sample(combineD) { _, b -> b }
+ sampleS.nextDeferred()
+ }
+ println("launching")
+ runCurrent()
+
+ println("emitting update")
+ updater.emit(10)
+ runCurrent()
+
+ println("emitting sampler")
+ emitter.emit(Unit)
+ runCurrent()
+
+ println("asserting")
+ assertEquals(20 to 10, result.await())
+ }
+
+ @Test
+ fun switchMapPromptly() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ val result =
+ activateSpecWithResult(network) {
+ emitter
+ .map { emitter.map { 1 }.map { it + 1 }.map { it * 2 } }
+ .hold(emptyTFlow)
+ .switchPromptly()
+ .nextDeferred()
+ }
+ runCurrent()
+
+ emitter.emit(Unit)
+ runCurrent()
+
+ assertTrue("Not complete", result.isCompleted)
+ assertEquals(4, result.await())
+ }
+
+ @Test
+ fun switchDeeper() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ val e2 = network.mutableTFlow<Unit>()
+ val result =
+ activateSpecWithResult(network) {
+ val tres =
+ merge(e2.map { 1 }, e2.map { 2 }, transformCoincidence = { a, b -> a + b })
+ tres.observeBuild()
+ val switch = emitter.map { tres }.flatten()
+ merge(switch, e2.map { null }, transformCoincidence = { a, _ -> a })
+ .filterNotNull()
+ .nextDeferred()
+ }
+ runCurrent()
+
+ emitter.emit(Unit)
+ runCurrent()
+
+ e2.emit(Unit)
+ runCurrent()
+
+ assertTrue("Not complete", result.isCompleted)
+ assertEquals(3, result.await())
+ }
+
+ @Test
+ fun recursionBasic() = runFrpTest { network ->
+ val add1 = network.mutableTFlow<Unit>()
+ val sub1 = network.mutableTFlow<Unit>()
+ val stepResult: StateFlow<Int> =
+ activateSpecWithResult(network) {
+ val dSum = TStateLoop<Int>()
+ val sAdd1 = add1.sample(dSum) { _, sum -> sum + 1 }
+ val sMinus1 = sub1.sample(dSum) { _, sum -> sum - 1 }
+ dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.hold(0)
+ dSum.toStateFlow()
+ }
+ runCurrent()
+
+ add1.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult.value)
+
+ add1.emit(Unit)
+ runCurrent()
+
+ assertEquals(2, stepResult.value)
+
+ sub1.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult.value)
+ }
+
+ @Test
+ fun recursiveTState() = runFrpTest { network ->
+ val e = network.mutableTFlow<Unit>()
+ var changes = 0
+ val state =
+ activateSpecWithResult(network) {
+ val s = TFlowLoop<Unit>()
+ val deferred = s.map { tStateOf(null) }
+ val e3 = e.map { tStateOf(Unit) }
+ val flattened = e3.mergeWith(deferred) { a, _ -> a }.hold(tStateOf(null)).flatten()
+ s.loopback = emptyTFlow
+ flattened.toStateFlow()
+ }
+
+ backgroundScope.launch { state.collect { changes++ } }
+ runCurrent()
+ }
+
+ @Test
+ fun fanOut() = runFrpTest { network ->
+ val e = network.mutableTFlow<Map<String, Int>>()
+ val (fooFlow, barFlow) =
+ activateSpecWithResult(network) {
+ val selector = e.groupByKey()
+ val foos = selector.eventsForKey("foo")
+ val bars = selector.eventsForKey("bar")
+ foos.toSharedFlow() to bars.toSharedFlow()
+ }
+ val stateFlow = fooFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null)
+ backgroundScope.launch { barFlow.collect { error("unexpected bar") } }
+ runCurrent()
+
+ assertEquals(null, stateFlow.value)
+
+ e.emit(mapOf("foo" to 1))
+ runCurrent()
+
+ assertEquals(1, stateFlow.value)
+ }
+
+ @Test
+ fun fanOutLateSubscribe() = runFrpTest { network ->
+ val e = network.mutableTFlow<Map<String, Int>>()
+ val barFlow =
+ activateSpecWithResult(network) {
+ val selector = e.groupByKey()
+ selector
+ .eventsForKey("foo")
+ .map { selector.eventsForKey("bar") }
+ .hold(emptyTFlow)
+ .switchPromptly()
+ .toSharedFlow()
+ }
+ val stateFlow = barFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null)
+ runCurrent()
+
+ assertEquals(null, stateFlow.value)
+
+ e.emit(mapOf("foo" to 0, "bar" to 1))
+ runCurrent()
+
+ assertEquals(1, stateFlow.value)
+ }
+
+ @Test
+ fun inputFlowCompleted() = runFrpTest { network ->
+ val results = mutableListOf<Int>()
+ val e = network.mutableTFlow<Int>()
+ activateSpec(network) { e.nextOnly().observe { results.add(it) } }
+ runCurrent()
+
+ e.emit(10)
+ runCurrent()
+
+ assertEquals(listOf(10), results)
+
+ e.emit(20)
+ runCurrent()
+ assertEquals(listOf(10), results)
+ }
+
+ @Test
+ fun fanOutThenMergeIncrementally() = runFrpTest { network ->
+ // A tflow of group updates, where a group is a tflow of child updates, where a child is a
+ // stateflow
+ val e = network.mutableTFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<StateFlow<String>>>>>>>()
+ println("fanOutMergeInc START")
+ val state =
+ activateSpecWithResult(network) {
+ // Convert nested Flows to nested TFlow/TState
+ val emitter: TFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>>> =
+ e.mapBuild { m ->
+ m.mapValues { (_, mFlow) ->
+ mFlow.map {
+ it.mapBuild { m2 ->
+ m2.mapValues { (_, mState) ->
+ mState.map { stateFlow -> stateFlow.toTState() }
+ }
+ }
+ }
+ }
+ }
+ // Accumulate all of our updates into a single TState
+ val accState: TState<Map<Int, Map<Int, String>>> =
+ emitter
+ .mapStateful {
+ changeMap: Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>> ->
+ changeMap.mapValues { (groupId, mGroupChanges) ->
+ mGroupChanges.map {
+ groupChanges: TFlow<Map<Int, Maybe<TState<String>>>> ->
+ // New group
+ val childChangeById = groupChanges.groupByKey()
+ val map: TFlow<Map<Int, Maybe<TFlow<Maybe<TState<String>>>>>> =
+ groupChanges.mapStateful {
+ gChangeMap: Map<Int, Maybe<TState<String>>> ->
+ gChangeMap.mapValues { (childId, mChild) ->
+ mChild.map { child: TState<String> ->
+ println("new child $childId in the house")
+ // New child
+ val eRemoved =
+ childChangeById
+ .eventsForKey(childId)
+ .filter { it === None }
+ .nextOnly()
+
+ val addChild: TFlow<Maybe<TState<String>>> =
+ now.map { mChild }
+ .onEach {
+ println(
+ "addChild (groupId=$groupId, childId=$childId) ${child.sample()}"
+ )
+ }
+
+ val removeChild: TFlow<Maybe<TState<String>>> =
+ eRemoved
+ .onEach {
+ println(
+ "removeChild (groupId=$groupId, childId=$childId)"
+ )
+ }
+ .map { none() }
+
+ addChild.mergeWith(removeChild) { _, _ ->
+ error("unexpected coincidence")
+ }
+ }
+ }
+ }
+ val mergeIncrementally: TFlow<Map<Int, Maybe<TState<String>>>> =
+ map.onEach { println("merge patch: $it") }
+ .mergeIncrementallyPromptly()
+ mergeIncrementally
+ .onEach { println("patch: $it") }
+ .foldMapIncrementally()
+ .flatMap { it.combineValues() }
+ }
+ }
+ }
+ .foldMapIncrementally()
+ .flatMap { it.combineValues() }
+
+ accState.toStateFlow()
+ }
+ runCurrent()
+
+ assertEquals(emptyMap(), state.value)
+
+ val emitter2 = network.mutableTFlow<Map<Int, Maybe<StateFlow<String>>>>()
+ println()
+ println("init outer 0")
+ e.emit(mapOf(0 to just(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)"))))
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value)
+
+ // replace
+ println()
+ println("replace inner 10")
+ emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)"))))
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value)
+
+ // remove
+ emitter2.emit(mapOf(10 to none()))
+ runCurrent()
+
+ assertEquals(mapOf(0 to emptyMap()), state.value)
+
+ // add again
+ emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)"))))
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
+
+ // batch update
+ emitter2.emit(
+ mapOf(
+ 10 to none(),
+ 11 to just(MutableStateFlow("(0, 11)")),
+ 12 to just(MutableStateFlow("(0, 12)")),
+ )
+ )
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(11 to "(0, 11)", 12 to "(0, 12)")), state.value)
+ }
+
+ @Test
+ fun applyLatestNetworkChanges() = runFrpTest { network ->
+ val newCount = network.mutableTFlow<FrpSpec<Flow<Int>>>()
+ val flowOfFlows: Flow<Flow<Int>> =
+ activateSpecWithResult(network) { newCount.applyLatestSpec().toSharedFlow() }
+ runCurrent()
+
+ val incCount = network.mutableTFlow<Unit>()
+ fun newFlow(): FrpSpec<SharedFlow<Int>> = frpSpec {
+ launchEffect {
+ try {
+ println("new flow!")
+ awaitCancellation()
+ } finally {
+ println("cancelling old flow")
+ }
+ }
+ lateinit var count: TState<Int>
+ count =
+ incCount
+ .onEach { println("incrementing ${count.sample()}") }
+ .fold(0) { _, c -> c + 1 }
+ count.stateChanges.toSharedFlow()
+ }
+
+ var outerCount = 0
+ val lastFlows: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+ flowOfFlows
+ .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
+ .pairwise(MutableStateFlow(null))
+ .onEach { outerCount++ }
+ .stateIn(
+ backgroundScope,
+ SharingStarted.Eagerly,
+ MutableStateFlow(null) to MutableStateFlow(null),
+ )
+
+ runCurrent()
+
+ newCount.emit(newFlow())
+ runCurrent()
+
+ assertEquals(1, outerCount)
+ // assertEquals(1, incCount.subscriptionCount)
+ assertNull(lastFlows.value.second.value)
+
+ incCount.emit(Unit)
+ runCurrent()
+
+ println("checking")
+ assertEquals(1, lastFlows.value.second.value)
+
+ incCount.emit(Unit)
+ runCurrent()
+
+ assertEquals(2, lastFlows.value.second.value)
+
+ newCount.emit(newFlow())
+ runCurrent()
+ incCount.emit(Unit)
+ runCurrent()
+
+ // verify old flow is not getting updates
+ assertEquals(2, lastFlows.value.first.value)
+ // but the new one is
+ assertEquals(1, lastFlows.value.second.value)
+ }
+
+ @Test
+ fun effect() = runFrpTest { network ->
+ val input = network.mutableTFlow<Unit>()
+ var effectRunning = false
+ var count = 0
+ activateSpec(network) {
+ val j = launchEffect {
+ effectRunning = true
+ try {
+ awaitCancellation()
+ } finally {
+ effectRunning = false
+ }
+ }
+ merge(emptyTFlow, input.nextOnly()).observe {
+ count++
+ j.cancel()
+ }
+ }
+ runCurrent()
+ assertEquals(true, effectRunning)
+ assertEquals(0, count)
+
+ println("1")
+ input.emit(Unit)
+ assertEquals(false, effectRunning)
+ assertEquals(1, count)
+
+ println("2")
+ input.emit(Unit)
+ assertEquals(1, count)
+ println("3")
+ input.emit(Unit)
+ assertEquals(1, count)
+ }
+
+ private fun runFrpTest(
+ timeout: Duration = 3.seconds,
+ block: suspend TestScope.(FrpNetwork) -> Unit,
+ ) {
+ runTest(timeout = timeout) {
+ val network = backgroundScope.newFrpNetwork()
+ runCurrent()
+ block(network)
+ }
+ }
+
+ private fun TestScope.activateSpec(network: FrpNetwork, spec: FrpSpec<*>) =
+ backgroundScope.launch { network.activateSpec(spec) }
+
+ private suspend fun <R> TestScope.activateSpecWithResult(
+ network: FrpNetwork,
+ spec: FrpSpec<R>,
+ ): R =
+ CompletableDeferred<R>()
+ .apply { activateSpec(network) { complete(spec.applySpec()) } }
+ .await()
+}
+
+private fun <T> assertEquals(expected: T, actual: T) =
+ org.junit.Assert.assertEquals(expected, actual)
+
+private fun <A> Flow<A>.pairwise(init: A): Flow<Pair<A, A>> = flow {
+ var prev = init
+ collect {
+ emit(prev to it)
+ prev = it
+ }
+}
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index 994bdb5..6489905 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -206,7 +206,7 @@
// Inform that DND settings have changed on OS upgrade
// Package: android
- NOTE_ZEN_UPGRADE = 48;
+ NOTE_ZEN_UPGRADE = 48 [deprecated = true];
// Notification to suggest automatic battery saver.
// Package: android
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index 6b6b39d..a77ba62 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -47,7 +47,6 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedValue;
-import android.view.Display;
import android.view.DisplayInfo;
import android.view.MagnificationSpec;
import android.view.View;
@@ -1637,9 +1636,10 @@
* <strong>if scale is >= {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}</strong>.
* We assume if the scale is < {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}, there
* will be no obvious magnification effect.
+ * Only the value of the default display is persisted in user's settings.
*/
public void persistScale(int displayId) {
- final float scale = getScale(Display.DEFAULT_DISPLAY);
+ final float scale = getScale(displayId);
if (scale < MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
return;
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c87d516..c522b64 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -205,6 +205,7 @@
.verifyCallerCanExecuteAppFunction(
callingUid,
callingPid,
+ targetUser,
requestInternal.getCallingPackage(),
targetPackageName,
requestInternal.getClientRequest().getFunctionIdentifier())
@@ -492,7 +493,7 @@
Slog.e(TAG, "Failed to bind to the AppFunctionService");
safeExecuteAppFunctionCallback.onResult(
ExecuteAppFunctionResponse.newFailure(
- ExecuteAppFunctionResponse.RESULT_TIMED_OUT,
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
"Failed to bind the AppFunctionService.",
/* extras= */ null));
}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
index 3592ed5..5393b93 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidator.java
@@ -64,6 +64,9 @@
* {@link Manifest.permission#EXECUTE_APP_FUNCTIONS} granted. In some cases, app functions can
* still opt-out of caller having {@link Manifest.permission#EXECUTE_APP_FUNCTIONS}.
*
+ * @param callingUid The calling uid.
+ * @param callingPid The calling pid.
+ * @param targetUser The user which the caller is requesting to execute as.
* @param callerPackageName The calling package (as previously validated).
* @param targetPackageName The package that owns the app function to execute.
* @param functionId The id of the app function to execute.
@@ -72,6 +75,7 @@
AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction(
int callingUid,
int callingPid,
+ @NonNull UserHandle targetUser,
@NonNull String callerPackageName,
@NonNull String targetPackageName,
@NonNull String functionId);
diff --git a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
index 8b6251a..e85a70d 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/CallerValidatorImpl.java
@@ -93,6 +93,7 @@
public AndroidFuture<Boolean> verifyCallerCanExecuteAppFunction(
int callingUid,
int callingPid,
+ @NonNull UserHandle targetUser,
@NonNull String callerPackageName,
@NonNull String targetPackageName,
@NonNull String functionId) {
@@ -122,7 +123,10 @@
FutureAppSearchSession futureAppSearchSession =
new FutureAppSearchSessionImpl(
- mContext.getSystemService(AppSearchManager.class),
+ Objects.requireNonNull(
+ mContext
+ .createContextAsUser(targetUser, 0)
+ .getSystemService(AppSearchManager.class)),
THREAD_POOL_EXECUTOR,
new SearchContext.Builder(APP_FUNCTION_STATIC_METADATA_DB).build());
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index cd2dd3a..81ae717 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -42,6 +42,7 @@
import android.app.admin.DevicePolicyManager;
import android.app.compat.CompatChanges;
import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
import android.companion.virtual.ActivityPolicyExemption;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.IVirtualDeviceActivityListener;
@@ -153,6 +154,9 @@
private static final String PERSISTENT_ID_PREFIX_CDM_ASSOCIATION = "companion:";
+ private static final List<String> DEVICE_PROFILES_ALLOWING_MIRROR_DISPLAYS = List.of(
+ AssociationRequest.DEVICE_PROFILE_APP_STREAMING);
+
/**
* Timeout until {@link #launchPendingIntent} stops waiting for an activity to be launched.
*/
@@ -498,6 +502,10 @@
return mAssociationInfo == null ? mParams.getName() : mAssociationInfo.getDisplayName();
}
+ String getDeviceProfile() {
+ return mAssociationInfo == null ? null : mAssociationInfo.getDeviceProfile();
+ }
+
/** Returns the public representation of the device. */
VirtualDevice getPublicVirtualDeviceObject() {
return mPublicVirtualDeviceObject;
@@ -1294,6 +1302,11 @@
return hasCustomAudioInputSupportInternal();
}
+ @Override
+ public boolean canCreateMirrorDisplays() {
+ return DEVICE_PROFILES_ALLOWING_MIRROR_DISPLAYS.contains(getDeviceProfile());
+ }
+
private boolean hasCustomAudioInputSupportInternal() {
if (!Flags.vdmPublicApis()) {
return false;
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 9060250..2acedd5 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -47,6 +47,7 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
+import android.util.EventLog;
import android.util.IndentingPrintWriter;
import android.util.LongArrayQueue;
import android.util.Slog;
@@ -200,6 +201,13 @@
// aborted.
private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt";
+ /**
+ * EventLog tags used when logging into the event log. Note the values must be sync with
+ * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
+ * name translation.
+ */
+ private static final int LOG_TAG_RESCUE_NOTE = 2900;
+
private static final Object sPackageWatchdogLock = new Object();
@GuardedBy("sPackageWatchdogLock")
private static PackageWatchdog sPackageWatchdog;
@@ -2024,7 +2032,7 @@
} else {
int count = getCount() + 1;
setCount(count);
- EventLogTags.writeRescueNote(Process.ROOT_UID, count, window);
+ EventLog.writeEvent(LOG_TAG_RESCUE_NOTE, Process.ROOT_UID, count, window);
if (Flags.recoverabilityDetection()) {
// After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply
// mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT.
diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java
index ada1953..feb5775 100644
--- a/services/core/java/com/android/server/RescueParty.java
+++ b/services/core/java/com/android/server/RescueParty.java
@@ -42,6 +42,7 @@
import android.sysprop.CrashRecoveryProperties;
import android.text.TextUtils;
import android.util.ArraySet;
+import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
@@ -154,6 +155,14 @@
private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
| ApplicationInfo.FLAG_SYSTEM;
+ /**
+ * EventLog tags used when logging into the event log. Note the values must be sync with
+ * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
+ * name translation.
+ */
+ private static final int LOG_TAG_RESCUE_SUCCESS = 2902;
+ private static final int LOG_TAG_RESCUE_FAILURE = 2903;
+
/** Register the Rescue Party observer as a Package Watchdog health observer */
public static void registerHealthObserver(Context context) {
PackageWatchdog.getInstance(context).registerHealthObserver(
@@ -523,7 +532,7 @@
Slog.w(TAG, "Attempting rescue level " + levelToString(level));
try {
executeRescueLevelInternal(context, level, failedPackage);
- EventLogTags.writeRescueSuccess(level);
+ EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level);
String successMsg = "Finished rescue level " + levelToString(level);
if (!TextUtils.isEmpty(failedPackage)) {
successMsg += " for package " + failedPackage;
@@ -704,7 +713,7 @@
private static void logRescueException(int level, @Nullable String failedPackageName,
Throwable t) {
final String msg = getCompleteMessage(t);
- EventLogTags.writeRescueFailure(level, msg);
+ EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg);
String failureMsg = "Failed rescue level " + levelToString(level);
if (!TextUtils.isEmpty(failedPackageName)) {
failureMsg += " for package " + failedPackageName;
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index a459ea9..ce66dc3 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -114,6 +114,9 @@
"options": [
{
"include-filter": "android.os.storage.cts.StorageManagerTest"
+ },
+ {
+ "include-filter": "android.os.storage.cts.StorageStatsManagerTest"
}
]
}
@@ -173,15 +176,6 @@
"include-filter": "com.android.server.wm.BackgroundActivityStart*"
}
]
- },
- {
- "name": "CtsOsTestCases",
- "file_patterns": ["StorageManagerService\\.java"],
- "options": [
- {
- "include-filter": "android.os.storage.cts.StorageStatsManagerTest"
- }
- ]
}
]
}
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 3499a3a..0ca3b56 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -5062,6 +5062,8 @@
Log.e(TAG, String.format(tmpl, activityName, pkgName, mAccountType));
return false;
}
+ intent.setComponent(targetActivityInfo.getComponentName());
+ bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return true;
} finally {
Binder.restoreCallingIdentity(bid);
@@ -5083,14 +5085,15 @@
Bundle simulateBundle = p.readBundle();
p.recycle();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
- if (intent != null && intent.getClass() != Intent.class) {
- return false;
- }
Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT,
Intent.class);
if (intent == null) {
return (simulateIntent == null);
}
+ if (intent.getClass() != Intent.class || simulateIntent.getClass() != Intent.class) {
+ return false;
+ }
+
if (!intent.filterEquals(simulateIntent)) {
return false;
}
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 71b6456..aca6d0b 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -1005,7 +1005,8 @@
case (int) AppsStartInfoProto.Package.USERS:
AppStartInfoContainer container =
new AppStartInfoContainer(mAppStartInfoHistoryListSize);
- int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS);
+ int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS,
+ pkgName);
synchronized (mLock) {
mData.put(pkgName, uid, container);
}
@@ -1403,7 +1404,7 @@
proto.end(token);
}
- int readFromProto(ProtoInputStream proto, long fieldId)
+ int readFromProto(ProtoInputStream proto, long fieldId, String packageName)
throws IOException, WireTypeMismatchException, ClassNotFoundException {
long token = proto.start(fieldId);
for (int next = proto.nextField();
@@ -1418,6 +1419,7 @@
// have a create time.
ApplicationStartInfo info = new ApplicationStartInfo(0);
info.readFromProto(proto, AppsStartInfoProto.Package.User.APP_START_INFO);
+ info.setPackageName(packageName);
mInfos.add(info);
break;
case (int) AppsStartInfoProto.Package.User.MONITORING_ENABLED:
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index e0cf96f..c2e62d0 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -540,11 +540,11 @@
this.uid = uid;
}
+ @SuppressWarnings("GuardedBy")
public void clear() {
mAppOpsCheckingService.removeUid(uid);
for (int i = 0; i < pkgOps.size(); i++) {
- String packageName = pkgOps.keyAt(i);
- mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid));
+ packageRemovedLocked(uid, pkgOps.keyAt(i));
}
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 87504154..0475b94 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -384,11 +384,12 @@
/**
* Indicates if a Bluetooth SCO activation request owner is controlling
* the SCO audio state itself or not.
- * @param uid the UI of the SOC request owner app
+ * @param uid the UID of the SOC request owner app
* @return true if we should control SCO audio state, false otherwise
*/
private boolean shouldStartScoForUid(int uid) {
- return !(uid == Process.BLUETOOTH_UID || uid == Process.PHONE_UID);
+ return !(UserHandle.isSameApp(uid, Process.BLUETOOTH_UID)
+ || UserHandle.isSameApp(uid, Process.PHONE_UID));
}
@GuardedBy("mDeviceStateLock")
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 5fd12c2..09de894 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -385,11 +385,6 @@
|| !updatedDevice.getDeviceAddress().equals(ads.getDeviceAddress())) {
continue;
}
- if (mDeviceBroker.isSADevice(updatedDevice) == mDeviceBroker.isSADevice(ads)) {
- ads.setHasHeadTracker(updatedDevice.hasHeadTracker());
- ads.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
- ads.setSAEnabled(updatedDevice.isSAEnabled());
- }
ads.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
mDeviceBroker.postUpdatedAdiDeviceState(ads, false /*initSA*/);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 561030e..c37d471 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1583,8 +1583,11 @@
synchronized (mCachedAbsVolDrivingStreamsLock) {
mCachedAbsVolDrivingStreams.forEach((dev, stream) -> {
- mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true,
- stream);
+ boolean enabled = true;
+ if (dev == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
+ enabled = mAvrcpAbsVolSupported;
+ }
+ mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", enabled, stream);
});
}
}
@@ -4881,7 +4884,7 @@
if (absDev == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
enabled = mAvrcpAbsVolSupported;
}
- if (stream != streamType) {
+ if (stream != streamType || !enabled) {
mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/"",
enabled, streamType);
}
@@ -10097,9 +10100,6 @@
case MSG_INIT_SPATIALIZER:
onInitSpatializer();
- // the device inventory can only be synchronized after the
- // spatializer has been initialized
- mDeviceBroker.postSynchronizeAdiDevicesInInventory(null);
mAudioEventWakeLock.release();
break;
@@ -10383,10 +10383,10 @@
}
/*package*/ void setAvrcpAbsoluteVolumeSupported(boolean support) {
- mAvrcpAbsVolSupported = support;
- if (absVolumeIndexFix()) {
- int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
- synchronized (mCachedAbsVolDrivingStreamsLock) {
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mAvrcpAbsVolSupported = support;
+ if (absVolumeIndexFix()) {
+ int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> {
if (!mAvrcpAbsVolSupported) {
mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
@@ -12499,6 +12499,12 @@
pw.println("\nLoudness alignment:");
mLoudnessCodecHelper.dump(pw);
+ pw.println("\nAbsolute voume devices:");
+ synchronized (mCachedAbsVolDrivingStreamsLock) {
+ mCachedAbsVolDrivingStreams.forEach((dev, stream) -> pw.println(
+ "Device type: 0x" + Integer.toHexString(dev) + ", driving stream " + stream));
+ }
+
mAudioSystem.dump(pw);
}
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index abfbddc..3afecf1 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -879,6 +879,14 @@
);
break;
+ case BiometricPrompt.DISMISSED_REASON_ERROR_NO_WM:
+ mClientReceiver.onError(
+ getEligibleModalities(),
+ BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,
+ 0 /* vendorCode */
+ );
+ break;
+
default:
Slog.w(TAG, "Unhandled reason: " + reason);
break;
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index de7bce7..8734136 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -253,9 +253,15 @@
// Check if any of the non-biometric and non-credential bits are set. If so, this is
// invalid.
- final int testBits = ~(Authenticators.DEVICE_CREDENTIAL
- | Authenticators.BIOMETRIC_MIN_STRENGTH
- | Authenticators.MANDATORY_BIOMETRICS);
+ final int testBits;
+ if (Flags.mandatoryBiometrics()) {
+ testBits = ~(Authenticators.DEVICE_CREDENTIAL
+ | Authenticators.BIOMETRIC_MIN_STRENGTH
+ | Authenticators.MANDATORY_BIOMETRICS);
+ } else {
+ testBits = ~(Authenticators.DEVICE_CREDENTIAL
+ | Authenticators.BIOMETRIC_MIN_STRENGTH);
+ }
if ((authenticators & testBits) != 0) {
Slog.e(BiometricService.TAG, "Non-biometric, non-credential bits found."
+ " Authenticators: " + authenticators);
diff --git a/services/core/java/com/android/server/display/DisplayGroup.java b/services/core/java/com/android/server/display/DisplayGroup.java
index 2dcd5cc..f73b66c 100644
--- a/services/core/java/com/android/server/display/DisplayGroup.java
+++ b/services/core/java/com/android/server/display/DisplayGroup.java
@@ -87,4 +87,14 @@
int getIdLocked(int index) {
return mDisplays.get(index).getDisplayIdLocked();
}
+
+ /** Returns the IDs of the {@link LogicalDisplay}s belonging to the DisplayGroup. */
+ int[] getIdsLocked() {
+ final int numDisplays = mDisplays.size();
+ final int[] displayIds = new int[numDisplays];
+ for (int i = 0; i < numDisplays; i++) {
+ displayIds[i] = mDisplays.get(i).getDisplayIdLocked();
+ }
+ return displayIds;
+ }
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ec1ec3a..99a7743 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1661,33 +1661,49 @@
return false;
}
+ private boolean hasVideoOutputPermission(String func) {
+ return checkCallingPermission(CAPTURE_VIDEO_OUTPUT, func)
+ || hasSecureVideoOutputPermission(func);
+ }
+
+ private boolean hasSecureVideoOutputPermission(String func) {
+ return checkCallingPermission(CAPTURE_SECURE_VIDEO_OUTPUT, func);
+ }
+
+ private boolean canCreateMirrorDisplays(IVirtualDevice virtualDevice) {
+ if (virtualDevice == null) {
+ return false;
+ }
+ try {
+ return virtualDevice.canCreateMirrorDisplays();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to query virtual device for permissions", e);
+ return false;
+ }
+ }
+
private boolean canProjectVideo(IMediaProjection projection) {
- if (projection != null) {
- try {
- if (projection.canProjectVideo()) {
- return true;
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "Unable to query projection service for permissions", e);
- }
+ if (projection == null) {
+ return false;
}
- if (checkCallingPermission(CAPTURE_VIDEO_OUTPUT, "canProjectVideo()")) {
- return true;
+ try {
+ return projection.canProjectVideo();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to query projection service for permissions", e);
+ return false;
}
- return canProjectSecureVideo(projection);
}
private boolean canProjectSecureVideo(IMediaProjection projection) {
- if (projection != null) {
- try {
- if (projection.canProjectSecureVideo()) {
- return true;
- }
- } catch (RemoteException e) {
- Slog.e(TAG, "Unable to query projection service for permissions", e);
- }
+ if (projection == null) {
+ return false;
}
- return checkCallingPermission(CAPTURE_SECURE_VIDEO_OUTPUT, "canProjectSecureVideo()");
+ try {
+ return projection.canProjectSecureVideo();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to query projection service for permissions", e);
+ return false;
+ }
}
private boolean checkCallingPermission(String permission, String func) {
@@ -1793,7 +1809,8 @@
&& (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
// Only a valid media projection or a virtual device can create a mirror virtual
// display.
- if (!canProjectVideo(projection) && virtualDevice == null) {
+ if (!canProjectVideo(projection) && !canCreateMirrorDisplays(virtualDevice)
+ && !hasVideoOutputPermission("createVirtualDisplayInternal")) {
throw new SecurityException("Requires CAPTURE_VIDEO_OUTPUT or "
+ "CAPTURE_SECURE_VIDEO_OUTPUT permission, or an appropriate "
+ "MediaProjection token in order to create a screen sharing virtual "
@@ -1803,7 +1820,8 @@
}
}
if (callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0) {
- if (!canProjectSecureVideo(projection)) {
+ if (!canProjectSecureVideo(projection)
+ && !hasSecureVideoOutputPermission("createVirtualDisplayInternal")) {
throw new SecurityException("Requires CAPTURE_SECURE_VIDEO_OUTPUT "
+ "or an appropriate MediaProjection token to create a "
+ "secure virtual display.");
@@ -2093,16 +2111,6 @@
}
}
- private void setVirtualDisplayStateInternal(IBinder appToken, boolean isOn) {
- synchronized (mSyncRoot) {
- if (mVirtualDisplayAdapter == null) {
- return;
- }
-
- mVirtualDisplayAdapter.setVirtualDisplayStateLocked(appToken, isOn);
- }
- }
-
private void setVirtualDisplayRotationInternal(IBinder appToken,
@Surface.Rotation int rotation) {
int displayId;
@@ -4615,16 +4623,6 @@
}
@Override // Binder call
- public void setVirtualDisplayState(IVirtualDisplayCallback callback, boolean isOn) {
- final long token = Binder.clearCallingIdentity();
- try {
- setVirtualDisplayStateInternal(callback.asBinder(), isOn);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
-
- @Override // Binder call
public void setVirtualDisplayRotation(IVirtualDisplayCallback callback,
@Surface.Rotation int rotation) {
if (!android.companion.virtualdevice.flags.Flags.virtualDisplayRotationApi()) {
@@ -5589,6 +5587,20 @@
}
@Override
+ public int[] getDisplayIdsForGroup(int groupId) {
+ synchronized (mSyncRoot) {
+ return mLogicalDisplayMapper.getDisplayIdsForGroupLocked(groupId);
+ }
+ }
+
+ @Override
+ public SparseArray<int[]> getDisplayIdsByGroupsIds() {
+ synchronized (mSyncRoot) {
+ return mLogicalDisplayMapper.getDisplayIdsByGroupIdLocked();
+ }
+ }
+
+ @Override
public IntArray getDisplayIds() {
IntArray displayIds = new IntArray();
synchronized (mSyncRoot) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index c3f6a52..06a9103 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -344,6 +344,23 @@
return displayIds;
}
+ public int[] getDisplayIdsForGroupLocked(int groupId) {
+ DisplayGroup displayGroup = mDisplayGroups.get(groupId);
+ if (displayGroup == null) {
+ return new int[]{};
+ }
+ return displayGroup.getIdsLocked();
+ }
+
+ public SparseArray<int[]> getDisplayIdsByGroupIdLocked() {
+ SparseArray<int[]> displayIdsByGroupIds = new SparseArray<>();
+ for (int i = 0; i < mDisplayGroups.size(); i++) {
+ final int displayGroupId = mDisplayGroups.keyAt(i);
+ displayIdsByGroupIds.put(displayGroupId, getDisplayIdsForGroupLocked(displayGroupId));
+ }
+ return displayIdsByGroupIds;
+ }
+
public void forEachLocked(Consumer<LogicalDisplay> consumer) {
forEachLocked(consumer, /* includeDisabled= */ true);
}
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 9b02f4b..e77c5ec 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -207,13 +207,6 @@
return device;
}
- void setVirtualDisplayStateLocked(IBinder appToken, boolean isOn) {
- VirtualDisplayDevice device = mVirtualDisplayDevices.get(appToken);
- if (device != null) {
- device.setDisplayState(isOn);
- }
- }
-
DisplayDevice getDisplayDevice(IBinder appToken) {
return mVirtualDisplayDevices.get(appToken);
}
@@ -273,7 +266,6 @@
private boolean mStopped;
private int mPendingChanges;
private Display.Mode mMode;
- private boolean mIsDisplayOn;
private int mDisplayIdToMirror;
private boolean mIsWindowManagerMirroring;
private DisplayCutout mDisplayCutout;
@@ -299,9 +291,8 @@
mCallback = callback;
mProjection = projection;
mMediaProjectionCallback = mediaProjectionCallback;
- mDisplayState = Display.STATE_UNKNOWN;
+ mDisplayState = Display.STATE_ON;
mPendingChanges |= PENDING_SURFACE_CHANGE;
- mIsDisplayOn = surface != null;
mDisplayIdToMirror = virtualDisplayConfig.getDisplayIdToMirror();
mIsWindowManagerMirroring = virtualDisplayConfig.isWindowManagerMirroringEnabled();
}
@@ -394,6 +385,8 @@
float sdrBrightnessState, DisplayOffloadSessionImpl displayOffloadSession) {
if (state != mDisplayState) {
mDisplayState = state;
+ mInfo = null;
+ sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED);
if (state == Display.STATE_OFF) {
mCallback.dispatchDisplayPaused();
} else {
@@ -416,12 +409,13 @@
public void setSurfaceLocked(Surface surface) {
if (!mStopped && mSurface != surface) {
- if ((mSurface != null) != (surface != null)) {
+ if (mDisplayState == Display.STATE_ON
+ && ((mSurface == null) != (surface == null))) {
+ mInfo = null;
sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED);
}
sendTraversalRequestLocked();
mSurface = surface;
- mInfo = null;
mPendingChanges |= PENDING_SURFACE_CHANGE;
}
}
@@ -439,14 +433,6 @@
}
}
- void setDisplayState(boolean isOn) {
- if (mIsDisplayOn != isOn) {
- mIsDisplayOn = isOn;
- mInfo = null;
- sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED);
- }
- }
-
public void stopLocked() {
Slog.d(TAG, "Virtual Display: stopping device " + mName);
setSurfaceLocked(null);
@@ -567,7 +553,11 @@
mInfo.touch = ((mFlags & VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH) == 0) ?
DisplayDeviceInfo.TOUCH_NONE : DisplayDeviceInfo.TOUCH_VIRTUAL;
- mInfo.state = mIsDisplayOn ? Display.STATE_ON : Display.STATE_OFF;
+ if (mSurface == null) {
+ mInfo.state = Display.STATE_OFF;
+ } else {
+ mInfo.state = mDisplayState;
+ }
mInfo.ownerUid = mOwnerUid;
mInfo.ownerPackageName = mOwnerPackageName;
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 91a4d6f..598901e 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -764,6 +764,7 @@
boolean hasPermissions(List<String> permissions) {
for (String permission : permissions) {
if (mContext.checkPermission(permission, mPid, mUid) != PERMISSION_GRANTED) {
+ Log.e(TAG, "no permission for " + permission);
return false;
}
}
@@ -919,6 +920,14 @@
}
}
if (curAuthState != newAuthState) {
+ if (newAuthState == AUTHORIZATION_DENIED
+ || newAuthState == AUTHORIZATION_DENIED_GRACE_PERIOD) {
+ Log.e(TAG, "updateNanoAppAuthState auth error: "
+ + Long.toHexString(nanoAppId) + ", "
+ + nanoappPermissions + ", "
+ + gracePeriodExpired + ", "
+ + forceDenied);
+ }
// Don't send the callback in the synchronized block or it could end up in a deadlock.
sendAuthStateCallback(nanoAppId, newAuthState);
}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index e1b8e9f..8b06dad 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1653,9 +1653,11 @@
manager));
}
+ List<MediaRoute2Info> routes =
+ userRecord.mHandler.mLastNotifiedRoutesToPrivilegedRouters.values().stream()
+ .toList();
userRecord.mHandler.sendMessage(
- obtainMessage(
- UserHandler::notifyInitialRoutesToManager, userRecord.mHandler, manager));
+ obtainMessage(ManagerRecord::notifyRoutesUpdated, managerRecord, routes));
}
@GuardedBy("mLock")
@@ -2433,6 +2435,51 @@
}
}
+ /**
+ * Notifies the corresponding manager of the availability of the given routes.
+ *
+ * @param routes The routes available to the manager that corresponds to this record.
+ */
+ public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
+ try {
+ mManager.notifyRoutesUpdated(routes);
+ } catch (RemoteException ex) {
+ Slog.w(TAG, "Failed to notify routes. Manager probably died.", ex);
+ }
+ }
+
+ /**
+ * Notifies the corresponding manager of an update in the given session.
+ *
+ * @param sessionInfo The updated session info.
+ */
+ public void notifySessionUpdated(RoutingSessionInfo sessionInfo) {
+ try {
+ mManager.notifySessionUpdated(sessionInfo);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "notifySessionUpdatedToManagers: Failed to notify. Manager probably died.",
+ ex);
+ }
+ }
+
+ /**
+ * Notifies the corresponding manager that the given session has been released.
+ *
+ * @param sessionInfo The released session info.
+ */
+ public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
+ try {
+ mManager.notifySessionReleased(sessionInfo);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "notifySessionReleasedToManagers: Failed to notify. Manager probably died.",
+ ex);
+ }
+ }
+
private void updateScanningState(@ScanningState int scanningState) {
if (mScanningState == scanningState) {
return;
@@ -2761,18 +2808,20 @@
getRouterRecords(/* hasSystemRoutingPermission= */ true);
List<RouterRecord> routerRecordsWithoutSystemRoutingPermission =
getRouterRecords(/* hasSystemRoutingPermission= */ false);
- List<IMediaRouter2Manager> managers = getManagers();
+ List<ManagerRecord> managers = getManagerRecords();
// Managers receive all provider updates with all routes.
- notifyRoutesUpdatedToManagers(
- managers, new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));
+ List<MediaRoute2Info> routesForPrivilegedRouters =
+ mLastNotifiedRoutesToPrivilegedRouters.values().stream().toList();
+ for (ManagerRecord manager : managers) {
+ manager.notifyRoutesUpdated(routesForPrivilegedRouters);
+ }
// Routers with system routing access (either via {@link MODIFY_AUDIO_ROUTING} or
// {@link BLUETOOTH_CONNECT} + {@link BLUETOOTH_SCAN}) receive all provider updates
// with all routes.
notifyRoutesUpdatedToRouterRecords(
- routerRecordsWithSystemRoutingPermission,
- new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));
+ routerRecordsWithSystemRoutingPermission, routesForPrivilegedRouters);
if (!isSystemProvider) {
// Regular routers receive updates from all non-system providers with all non-system
@@ -3068,8 +3117,10 @@
private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider,
@NonNull RoutingSessionInfo sessionInfo) {
- List<IMediaRouter2Manager> managers = getManagers();
- notifySessionUpdatedToManagers(managers, sessionInfo);
+ List<ManagerRecord> managers = getManagerRecords();
+ for (ManagerRecord manager : managers) {
+ manager.notifySessionUpdated(sessionInfo);
+ }
// For system provider, notify all routers.
if (provider == mSystemProvider) {
@@ -3093,8 +3144,10 @@
private void onSessionReleasedOnHandler(@NonNull MediaRoute2Provider provider,
@NonNull RoutingSessionInfo sessionInfo) {
- List<IMediaRouter2Manager> managers = getManagers();
- notifySessionReleasedToManagers(managers, sessionInfo);
+ List<ManagerRecord> managers = getManagerRecords();
+ for (ManagerRecord manager : managers) {
+ manager.notifySessionReleased(sessionInfo);
+ }
RouterRecord routerRecord = mSessionToRouterMap.get(sessionInfo.getId());
if (routerRecord == null) {
@@ -3169,20 +3222,6 @@
return true;
}
- private List<IMediaRouter2Manager> getManagers() {
- final List<IMediaRouter2Manager> managers = new ArrayList<>();
- MediaRouter2ServiceImpl service = mServiceRef.get();
- if (service == null) {
- return managers;
- }
- synchronized (service.mLock) {
- for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) {
- managers.add(managerRecord.mManager);
- }
- }
- return managers;
- }
-
private List<RouterRecord> getRouterRecords() {
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
@@ -3269,37 +3308,6 @@
}
}
- /**
- * Notifies {@code manager} with all known routes. This only happens once after {@code
- * manager} is registered through {@link #registerManager(IMediaRouter2Manager, String)
- * registerManager()}.
- *
- * @param manager {@link IMediaRouter2Manager} to be notified.
- */
- private void notifyInitialRoutesToManager(@NonNull IMediaRouter2Manager manager) {
- if (mLastNotifiedRoutesToPrivilegedRouters.isEmpty()) {
- return;
- }
- try {
- manager.notifyRoutesUpdated(
- new ArrayList<>(mLastNotifiedRoutesToPrivilegedRouters.values()));
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to notify all routes. Manager probably died.", ex);
- }
- }
-
- private void notifyRoutesUpdatedToManagers(
- @NonNull List<IMediaRouter2Manager> managers,
- @NonNull List<MediaRoute2Info> routes) {
- for (IMediaRouter2Manager manager : managers) {
- try {
- manager.notifyRoutesUpdated(routes);
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to notify routes changed. Manager probably died.", ex);
- }
- }
- }
-
private void notifySessionCreatedToManagers(long managerRequestId,
@NonNull RoutingSessionInfo session) {
int requesterId = toRequesterId(managerRequestId);
@@ -3317,32 +3325,6 @@
}
}
- private void notifySessionUpdatedToManagers(
- @NonNull List<IMediaRouter2Manager> managers,
- @NonNull RoutingSessionInfo sessionInfo) {
- for (IMediaRouter2Manager manager : managers) {
- try {
- manager.notifySessionUpdated(sessionInfo);
- } catch (RemoteException ex) {
- Slog.w(TAG, "notifySessionUpdatedToManagers: "
- + "Failed to notify. Manager probably died.", ex);
- }
- }
- }
-
- private void notifySessionReleasedToManagers(
- @NonNull List<IMediaRouter2Manager> managers,
- @NonNull RoutingSessionInfo sessionInfo) {
- for (IMediaRouter2Manager manager : managers) {
- try {
- manager.notifySessionReleased(sessionInfo);
- } catch (RemoteException ex) {
- Slog.w(TAG, "notifySessionReleasedToManagers: "
- + "Failed to notify. Manager probably died.", ex);
- }
- }
- }
-
private void notifyDiscoveryPreferenceChangedToManager(@NonNull RouterRecord routerRecord,
@NonNull IMediaRouter2Manager manager) {
try {
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 0a9109b..d752429 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -16,7 +16,6 @@
package com.android.server.media;
-import static android.media.MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
import static android.media.VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
import static android.media.VolumeProvider.VOLUME_CONTROL_FIXED;
import static android.media.VolumeProvider.VOLUME_CONTROL_RELATIVE;
@@ -48,9 +47,7 @@
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.MediaMetadata;
-import android.media.MediaRouter2Manager;
import android.media.Rating;
-import android.media.RoutingSessionInfo;
import android.media.VolumeProvider;
import android.media.session.ISession;
import android.media.session.ISessionCallback;
@@ -186,7 +183,6 @@
private final MediaSessionService mService;
private final UriGrantsManagerInternal mUgmInternal;
private final Context mContext;
- private final boolean mVolumeAdjustmentForRemoteGroupSessions;
private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
@@ -311,8 +307,6 @@
mAudioAttrs = DEFAULT_ATTRIBUTES;
mPolicies = policies;
mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
- mVolumeAdjustmentForRemoteGroupSessions = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
mForegroundServiceDelegationOptions = createForegroundServiceDelegationOptions();
@@ -659,49 +653,7 @@
}
return false;
}
- if (mVolumeAdjustmentForRemoteGroupSessions) {
- if (DEBUG) {
- Slog.d(
- TAG,
- "Volume adjustment for remote group sessions allowed so MediaSessionRecord"
- + " can handle volume key");
- }
- return true;
- }
- // See b/228021646 for details.
- MediaRouter2Manager mRouter2Manager = MediaRouter2Manager.getInstance(mContext);
- List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
- boolean foundNonSystemSession = false;
- boolean remoteSessionAllowVolumeAdjustment = true;
- if (DEBUG) {
- Slog.d(
- TAG,
- "Found "
- + sessions.size()
- + " routing sessions for package name "
- + mPackageName);
- }
- for (RoutingSessionInfo session : sessions) {
- if (DEBUG) {
- Slog.d(TAG, "Found routingSessionInfo: " + session);
- }
- if (!session.isSystemSession()) {
- foundNonSystemSession = true;
- if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
- remoteSessionAllowVolumeAdjustment = false;
- }
- }
- }
- if (!foundNonSystemSession) {
- if (DEBUG) {
- Slog.d(
- TAG,
- "Package " + mPackageName
- + " has a remote media session but no associated routing session");
- }
- }
-
- return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
+ return true;
}
@Override
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 47f579d..e7e519e 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -42,6 +42,8 @@
import android.app.IProcessObserver;
import android.app.KeyguardManager;
import android.app.compat.CompatChanges;
+import android.app.role.RoleManager;
+import android.companion.AssociationRequest;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.ComponentName;
@@ -94,7 +96,7 @@
/**
* Manages MediaProjection sessions.
- *
+ * <p>
* The {@link MediaProjectionManagerService} manages the creation and lifetime of MediaProjections,
* as well as the capabilities they grant. Any service using MediaProjection tokens as permission
* grants <b>must</b> validate the token before use by calling {@link
@@ -137,6 +139,7 @@
private final PackageManager mPackageManager;
private final WindowManagerInternal mWmInternal;
private final KeyguardManager mKeyguardManager;
+ private final RoleManager mRoleManager;
private final MediaRouter mMediaRouter;
private final MediaRouterCallback mMediaRouterCallback;
@@ -173,6 +176,7 @@
mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
mKeyguardManager.addKeyguardLockedStateListener(
mContext.getMainExecutor(), this::onKeyguardLockedStateChanged);
+ mRoleManager = mContext.getSystemService(RoleManager.class);
Watchdog.getInstance().addMonitor(this);
}
@@ -182,6 +186,7 @@
* - be one of the bugreport allowlisted packages, or
* - hold the OP_PROJECT_MEDIA AppOp.
*/
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean canCaptureKeyguard() {
if (!android.companion.virtualdevice.flags.Flags.mediaProjectionKeyguardRestrictions()) {
return true;
@@ -193,6 +198,9 @@
if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT,
mProjectionGrant.packageName)
== PackageManager.PERMISSION_GRANTED) {
+ Slog.v(TAG,
+ "Allowing keyguard capture for package with RECORD_SENSITIVE_CONTENT "
+ + "permission");
return true;
}
if (AppOpsManager.MODE_ALLOWED == mAppOps.noteOpNoThrow(AppOpsManager.OP_PROJECT_MEDIA,
@@ -200,6 +208,13 @@
"recording lockscreen")) {
// Some tools use media projection by granting the OP_PROJECT_MEDIA app
// op via a shell command. Those tools can be granted keyguard capture
+ Slog.v(TAG,
+ "Allowing keyguard capture for package with OP_PROJECT_MEDIA AppOp ");
+ return true;
+ }
+ if (isProjectionAppHoldingAppStreamingRoleLocked()) {
+ Slog.v(TAG,
+ "Allowing keyguard capture for package holding app streaming role.");
return true;
}
return SystemConfig.getInstance().getBugreportWhitelistedPackages()
@@ -698,6 +713,20 @@
}
}
+ /**
+ * Application holding the app streaming role
+ * ({@value AssociationRequest#DEVICE_PROFILE_APP_STREAMING}) are allowed to record the
+ * lockscreen.
+ *
+ * @return true if the is held by the recording application.
+ */
+ @GuardedBy("mLock")
+ private boolean isProjectionAppHoldingAppStreamingRoleLocked() {
+ return mRoleManager.getRoleHoldersAsUser(AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
+ mContext.getUser())
+ .contains(mProjectionGrant.packageName);
+ }
+
private void dump(final PrintWriter pw) {
pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 9d30c56..e8d14cb 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -1435,6 +1435,10 @@
return false;
}
+ if (record.getSbn().getNotification().isMediaNotification()) {
+ return false;
+ }
+
return true;
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 655f2e4..56e0a89 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4117,6 +4117,34 @@
}
@Override
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void setAdjustmentTypeSupportedState(INotificationListener token,
+ @Adjustment.Keys String key, boolean supported) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mNotificationLock) {
+ final ManagedServiceInfo info = mAssistants.checkServiceTokenLocked(token);
+ if (key == null) {
+ return;
+ }
+ mAssistants.setAdjustmentTypeSupportedState(info, key, supported);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public @NonNull List<String> getUnsupportedAdjustmentTypes() {
+ checkCallerIsSystemOrSystemUiOrShell();
+ synchronized (mNotificationLock) {
+ return new ArrayList(mAssistants.mNasUnsupported.getOrDefault(
+ UserHandle.getUserId(Binder.getCallingUid()), new HashSet<>()));
+ }
+ }
+
+ @Override
@FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
public boolean appCanBePromoted(String pkg, int uid) {
checkCallerIsSystemOrSystemUiOrShell();
@@ -7435,7 +7463,7 @@
NotificationRecord r = findNotificationLocked(pkg, null, notificationId, userId);
if (r != null) {
if (DBG) {
- final String type = (flag == FLAG_FOREGROUND_SERVICE) ? "FGS" : "UIJ";
+ final String type = (flag == FLAG_FOREGROUND_SERVICE) ? "FGS" : "UIJ";
Slog.d(TAG, "Remove " + type + " flag not allow. "
+ "Cancel " + type + " notification");
}
@@ -7452,7 +7480,11 @@
// strip flag from all enqueued notifications. listeners will be informed
// in post runnable.
StatusBarNotification sbn = r.getSbn();
- sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ if (notificationForceGrouping()) {
+ sbn.getNotification().flags = (r.getFlags() & ~flag);
+ } else {
+ sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ }
}
}
@@ -7461,7 +7493,11 @@
if (r != null) {
// if posted notification exists, strip its flag and tell listeners
StatusBarNotification sbn = r.getSbn();
- sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ if (notificationForceGrouping()) {
+ sbn.getNotification().flags = (r.getFlags() & ~flag);
+ } else {
+ sbn.getNotification().flags = (r.mOriginalFlags & ~flag);
+ }
mRankingHelper.sort(mNotificationList);
mListeners.notifyPostedLocked(r, r);
}
@@ -9459,6 +9495,28 @@
}
/**
+ * Check if the notification was a summary that has been auto-grouped
+ * @param r the current notification record
+ * @param old the previous notification record
+ * @return true if the notification record was a summary that was auto-grouped
+ */
+ @GuardedBy("mNotificationLock")
+ private boolean wasSummaryAutogrouped(NotificationRecord r, NotificationRecord old) {
+ boolean wasAutogrouped = false;
+ if (old != null) {
+ boolean wasSummary = (old.mOriginalFlags & FLAG_GROUP_SUMMARY) != 0;
+ boolean wasForcedGrouped = (old.getFlags() & FLAG_GROUP_SUMMARY) == 0
+ && old.getSbn().getOverrideGroupKey() != null;
+ boolean isNotAutogroupSummary = (r.getFlags() & FLAG_AUTOGROUP_SUMMARY) == 0
+ && (r.getFlags() & FLAG_GROUP_SUMMARY) != 0;
+ if ((wasSummary && wasForcedGrouped) || (wasForcedGrouped && isNotAutogroupSummary)) {
+ wasAutogrouped = true;
+ }
+ }
+ return wasAutogrouped;
+ }
+
+ /**
* Ensures that grouped notification receive their special treatment.
*
* <p>Cancels group children if the new notification causes a group to lose
@@ -9478,14 +9536,9 @@
}
if (notificationForceGrouping()) {
- if (old != null) {
- // If this is an update to a summary that was forced grouped => remove summary flag
- boolean wasSummary = (old.mOriginalFlags & FLAG_GROUP_SUMMARY) != 0;
- boolean wasForcedGrouped = (old.getFlags() & FLAG_GROUP_SUMMARY) == 0
- && old.getSbn().getOverrideGroupKey() != null;
- if (n.isGroupSummary() && wasSummary && wasForcedGrouped) {
- n.flags &= ~FLAG_GROUP_SUMMARY;
- }
+ // If this is an update to a summary that was forced grouped => remove summary flag
+ if (wasSummaryAutogrouped(r, old)) {
+ n.flags &= ~FLAG_GROUP_SUMMARY;
}
}
@@ -11333,12 +11386,16 @@
static final String TAG_ENABLED_NOTIFICATION_ASSISTANTS = "enabled_assistants";
private static final String ATT_TYPES = "types";
+ private static final String ATT_NAS_UNSUPPORTED = "unsupported_adjustments";
private final Object mLock = new Object();
@GuardedBy("mLock")
private Set<String> mAllowedAdjustments = new ArraySet<>();
+ @GuardedBy("mLock")
+ private Map<Integer, HashSet<String>> mNasUnsupported = new ArrayMap<>();
+
protected ComponentName mDefaultFromConfig = null;
@Override
@@ -11831,6 +11888,10 @@
setNotificationAssistantAccessGrantedForUserInternal(
currentComponent, userId, false, userSet);
}
+ } else {
+ if (android.service.notification.Flags.notificationClassification()) {
+ mNasUnsupported.put(userId, new HashSet<>());
+ }
}
super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet);
}
@@ -11838,6 +11899,63 @@
private boolean isVerboseLogEnabled() {
return Log.isLoggable("notification_assistant", Log.VERBOSE);
}
+
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ @GuardedBy("mNotificationLock")
+ public void setAdjustmentTypeSupportedState(ManagedServiceInfo info,
+ @Adjustment.Keys String key, boolean supported) {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return;
+ }
+ HashSet<String> disabledAdjustments =
+ mNasUnsupported.getOrDefault(info.userid, new HashSet<>());
+ if (supported) {
+ disabledAdjustments.remove(key);
+ } else {
+ disabledAdjustments.add(key);
+ }
+ mNasUnsupported.put(info.userid, disabledAdjustments);
+ handleSavePolicyFile();
+ }
+
+ @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ @GuardedBy("mNotificationLock")
+ public @NonNull Set<String> getUnsupportedAdjustments(@UserIdInt int userId) {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return new HashSet<>();
+ }
+ return mNasUnsupported.getOrDefault(userId, new HashSet<>());
+ }
+
+ @Override
+ protected void writeExtraAttributes(TypedXmlSerializer out, @UserIdInt int approvedUserId)
+ throws IOException {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mNasUnsupported.containsKey(approvedUserId)) {
+ out.attribute(null, ATT_NAS_UNSUPPORTED,
+ TextUtils.join(",", mNasUnsupported.get(approvedUserId)));
+ }
+ }
+ }
+
+ @Override
+ protected void readExtraAttributes(String tag, TypedXmlPullParser parser,
+ @UserIdInt int approvedUserId) throws IOException {
+ if (!android.service.notification.Flags.notificationClassification()) {
+ return;
+ }
+ if (ManagedServices.TAG_MANAGED_SERVICES.equals(tag)) {
+ final String types = XmlUtils.readStringAttribute(parser, ATT_NAS_UNSUPPORTED);
+ synchronized (mLock) {
+ if (!TextUtils.isEmpty(types)) {
+ mNasUnsupported.put(approvedUserId, new HashSet(List.of(types.split(","))));
+ }
+ }
+ }
+ }
}
/**
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index ff263d1..bdca555 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -37,7 +37,6 @@
import android.util.ArraySet;
import android.util.Slog;
-import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.util.NotificationMessagingUtil;
import java.io.PrintWriter;
@@ -173,13 +172,6 @@
maybeLogInterceptDecision(record, false, "criticalNotification");
return false;
}
- // Make an exception to policy for the notification saying that policy has changed
- if (NotificationManager.Policy.areAllVisualEffectsSuppressed(policy.suppressedVisualEffects)
- && "android".equals(record.getSbn().getPackageName())
- && SystemMessageProto.SystemMessage.NOTE_ZEN_UPGRADE == record.getSbn().getId()) {
- maybeLogInterceptDecision(record, false, "systemDndChangedNotification");
- return false;
- }
switch (zen) {
case Global.ZEN_MODE_NO_INTERRUPTIONS:
// #notevenalarms
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 626c3dd..ea211a9 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -54,10 +54,8 @@
import android.app.AppOpsManager;
import android.app.AutomaticZenRule;
import android.app.Flags;
-import android.app.Notification;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
-import android.app.PendingIntent;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
@@ -74,7 +72,6 @@
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.database.ContentObserver;
-import android.graphics.drawable.Icon;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioManagerInternal;
@@ -90,7 +87,6 @@
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
-import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.notification.Condition;
import android.service.notification.ConditionProviderService;
@@ -117,8 +113,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
import com.android.internal.logging.MetricsLogger;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
@@ -309,7 +303,6 @@
mHandler.postMetricsTimer();
cleanUpZenRules();
mIsSystemServicesReady = true;
- showZenUpgradeNotification(mZenMode);
}
/**
@@ -485,7 +478,7 @@
populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true);
rule = maybeRestoreRemovedRule(newConfig, pkg, rule, automaticZenRule, origin);
newConfig.automaticRules.put(rule.id, rule);
- maybeReplaceDefaultRule(newConfig, automaticZenRule);
+ maybeReplaceDefaultRule(newConfig, null, automaticZenRule);
if (setConfigLocked(newConfig, origin, reason, rule.component, true, callingUid)) {
return rule.id;
@@ -535,13 +528,24 @@
return ruleToRestore;
}
- private static void maybeReplaceDefaultRule(ZenModeConfig config, AutomaticZenRule addedRule) {
+ /**
+ * Possibly delete built-in rules if a more suitable rule is added or updated.
+ *
+ * <p>Today, this is done in one case: delete a disabled "Sleeping" rule if a Bedtime Mode is
+ * added (or an existing mode is turned into {@link AutomaticZenRule#TYPE_BEDTIME}, when
+ * upgrading). Because only the {@code config_systemWellbeing} package is allowed to use rules
+ * of this type, this will not trigger wantonly.
+ *
+ * @param oldRule If non-null, {@code rule} is updating {@code oldRule}. Otherwise,
+ * {@code rule} is being added.
+ */
+ private static void maybeReplaceDefaultRule(ZenModeConfig config, @Nullable ZenRule oldRule,
+ AutomaticZenRule rule) {
if (!Flags.modesApi()) {
return;
}
- if (addedRule.getType() == AutomaticZenRule.TYPE_BEDTIME) {
- // Delete a built-in disabled "Sleeping" rule when a BEDTIME rule is added; it may have
- // smarter triggers and it will prevent confusion about which one to use.
+ if (rule.getType() == AutomaticZenRule.TYPE_BEDTIME
+ && (oldRule == null || oldRule.type != rule.getType())) {
// Note: we must not verify canManageAutomaticZenRule here, since most likely they
// won't have the same owner (sleeping - system; bedtime - DWB).
ZenRule sleepingRule = config.automaticRules.get(
@@ -589,6 +593,10 @@
// condition) when no changes happen.
return true;
}
+
+ if (Flags.modesUi()) {
+ maybeReplaceDefaultRule(newConfig, oldRule, automaticZenRule);
+ }
return setConfigLocked(newConfig, origin, reason,
newRule.component, true, callingUid);
}
@@ -1584,8 +1592,6 @@
String reason, @Nullable String caller, int callingUid) {
setManualZenMode(zenMode, conditionId, origin, reason, caller, true /*setRingerMode*/,
callingUid);
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 0);
}
private void setManualZenMode(int zenMode, Uri conditionId, @ConfigOrigin int origin,
@@ -1783,17 +1789,6 @@
SystemZenRules.maybeUpgradeRules(mContext, config);
}
- // Resolve user id for settings.
- userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId;
- if (config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE) {
- Settings.Secure.putIntForUser(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1, userId);
- } else {
- // devices not restoring/upgrading already have updated zen settings
- Settings.Secure.putIntForUser(mContext.getContentResolver(),
- Settings.Secure.ZEN_SETTINGS_UPDATED, 1, userId);
- }
-
if (Flags.modesApi() && forRestore) {
// Note: forBackup doesn't write deletedRules, but just in case.
config.deletedRules.clear();
@@ -2062,7 +2057,6 @@
Global.putInt(mContext.getContentResolver(), Global.ZEN_MODE, zen);
ZenLog.traceSetZenMode(Global.getInt(mContext.getContentResolver(), Global.ZEN_MODE, -1),
"updated setting");
- showZenUpgradeNotification(zen);
}
private int getPreviousRingerModeSetting() {
@@ -2117,12 +2111,6 @@
for (ZenRule automaticRule : mConfig.automaticRules.values()) {
if (automaticRule.isActive()) {
if (zenSeverity(automaticRule.zenMode) > zenSeverity(zen)) {
- // automatic rule triggered dnd and user hasn't seen update dnd dialog
- if (Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.ZEN_SETTINGS_SUGGESTION_VIEWED, 1) == 0) {
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_SETTINGS_SUGGESTION, 1);
- }
zen = automaticRule.zenMode;
}
}
@@ -2702,62 +2690,6 @@
}
}
- private void showZenUpgradeNotification(int zen) {
- final boolean isWatch = mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_WATCH);
- final boolean showNotification = mIsSystemServicesReady
- && zen != Global.ZEN_MODE_OFF
- && !isWatch
- && Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0) != 0
- && Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.ZEN_SETTINGS_UPDATED, 0) != 1;
-
- if (isWatch) {
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
- }
-
- if (showNotification) {
- mNotificationManager.notify(TAG, SystemMessage.NOTE_ZEN_UPGRADE,
- createZenUpgradeNotification());
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
- }
- }
-
- @VisibleForTesting
- protected Notification createZenUpgradeNotification() {
- final Bundle extras = new Bundle();
- extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
- mContext.getResources().getString(R.string.global_action_settings));
- int title = R.string.zen_upgrade_notification_title;
- int content = R.string.zen_upgrade_notification_content;
- int drawable = R.drawable.ic_zen_24dp;
- if (NotificationManager.Policy.areAllVisualEffectsSuppressed(
- getConsolidatedNotificationPolicy().suppressedVisualEffects)) {
- title = R.string.zen_upgrade_notification_visd_title;
- content = R.string.zen_upgrade_notification_visd_content;
- drawable = R.drawable.ic_dnd_block_notifications;
- }
-
- Intent onboardingIntent = new Intent(Settings.ZEN_MODE_ONBOARDING);
- onboardingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- return new Notification.Builder(mContext, SystemNotificationChannels.DO_NOT_DISTURB)
- .setAutoCancel(true)
- .setSmallIcon(R.drawable.ic_settings_24dp)
- .setLargeIcon(Icon.createWithResource(mContext, drawable))
- .setContentTitle(mContext.getResources().getString(title))
- .setContentText(mContext.getResources().getString(content))
- .setContentIntent(PendingIntent.getActivity(mContext, 0, onboardingIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))
- .setAutoCancel(true)
- .setLocalOnly(true)
- .addExtras(extras)
- .setStyle(new Notification.BigTextStyle())
- .build();
- }
-
private int drawableResNameToResId(String packageName, String resourceName) {
if (TextUtils.isEmpty(resourceName)) {
return 0;
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 89ced12..4665a72 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2208,10 +2208,10 @@
return true;
}
boolean permissionGranted = requireFullPermission ? hasPermission(
- Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL)
: (hasPermission(
- android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
- || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
if (!permissionGranted) {
if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 1569fa0..02afdd6 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -80,6 +80,7 @@
import com.android.server.pm.PackageDexOptimizer.DexOptResult;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
+import com.android.server.pm.local.PackageManagerLocalImpl;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.PackageStateInternal;
@@ -819,10 +820,16 @@
final PackageSetting ps = installRequest.getScannedPackageSetting();
final String packageName = ps.getPackageName();
+ PackageSetting uncommittedPs = null;
+ if (Flags.improveInstallFreeze()) {
+ uncommittedPs = ps;
+ }
+
PackageManagerLocal packageManagerLocal =
LocalManagerRegistry.getManager(PackageManagerLocal.class);
try (PackageManagerLocal.FilteredSnapshot snapshot =
- packageManagerLocal.withFilteredSnapshot()) {
+ PackageManagerLocalImpl.withFilteredSnapshot(packageManagerLocal,
+ uncommittedPs)) {
boolean ignoreDexoptProfile =
(installRequest.getInstallFlags()
& PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE)
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index aca65bf..83292b7 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -148,6 +148,7 @@
import android.util.ArraySet;
import android.util.EventLog;
import android.util.ExceptionUtils;
+import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -1014,13 +1015,17 @@
final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
try {
CriticalEventLog.getInstance().logInstallPackagesStarted();
-
if (prepareInstallPackages(requests)
&& scanInstallPackages(requests, createdAppId, versionInfos)) {
List<ReconciledPackage> reconciledPackages =
reconcileInstallPackages(requests, versionInfos);
- if (reconciledPackages != null
- && renameAndUpdatePaths(requests)
+ if (reconciledPackages == null) {
+ return;
+ }
+ if (Flags.improveInstallFreeze()) {
+ prepPerformDexoptIfNeeded(reconciledPackages);
+ }
+ if (renameAndUpdatePaths(requests)
&& commitInstallPackages(reconciledPackages)) {
success = true;
}
@@ -1031,6 +1036,75 @@
}
}
+ private int[] getNewUsers(InstallRequest installRequest, int[] allUsers)
+ throws PackageManagerException {
+ final int userId = installRequest.getUserId();
+ if (userId != UserHandle.USER_ALL && userId != UserHandle.USER_CURRENT
+ && !mPm.mUserManager.exists(userId)) {
+ throw new PackageManagerException(PackageManagerException.INTERNAL_ERROR_MISSING_USER,
+ "User " + userId + " doesn't exist or has been removed");
+ }
+
+ final IntArray newUserIds = new IntArray();
+ if (userId != UserHandle.USER_ALL) {
+ newUserIds.add(userId);
+ } else if (allUsers != null) {
+ final int[] installedForUsers = installRequest.getOriginUsers();
+ for (int currentUserId : allUsers) {
+ final boolean installedForCurrentUser = ArrayUtils.contains(
+ installedForUsers, currentUserId);
+ final boolean restrictedByPolicy =
+ mPm.isUserRestricted(currentUserId,
+ UserManager.DISALLOW_INSTALL_APPS)
+ || mPm.isUserRestricted(currentUserId,
+ UserManager.DISALLOW_DEBUGGING_FEATURES);
+ if (installedForCurrentUser || !restrictedByPolicy) {
+ newUserIds.add(currentUserId);
+ }
+ }
+ }
+
+ if (newUserIds.size() == 0) {
+ throw new PackageManagerException(PackageManagerException.INTERNAL_ERROR_MISSING_USER,
+ "User " + userId + " doesn't exist or has been removed");
+ } else {
+ return newUserIds.toArray();
+ }
+ }
+
+ private void prepPerformDexoptIfNeeded(List<ReconciledPackage> reconciledPackages) {
+ for (ReconciledPackage reconciledPkg : reconciledPackages) {
+ final InstallRequest request = reconciledPkg.mInstallRequest;
+ // prepare profiles
+ final PackageSetting ps = request.getScannedPackageSetting();
+ final PackageSetting oldPkgSetting = request.getScanRequestOldPackageSetting();
+ final int[] allUsers = mPm.mUserManager.getUserIds();
+ if (reconciledPkg.mCollectedSharedLibraryInfos != null
+ || (oldPkgSetting != null
+ && !oldPkgSetting.getSharedLibraryDependencies().isEmpty())) {
+ // Reconcile if the new package or the old package uses shared libraries.
+ // It is possible that the old package uses shared libraries but the new
+ // one doesn't.
+ mSharedLibraries.executeSharedLibrariesUpdate(request.getParsedPackage(), ps,
+ null, null, reconciledPkg.mCollectedSharedLibraryInfos, allUsers);
+ }
+ try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
+ final int[] newUsers = getNewUsers(request, allUsers);
+ // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088)
+ mAppDataHelper.prepareAppDataPostCommitLIF(ps, 0, newUsers);
+ if (request.isClearCodeCache()) {
+ mAppDataHelper.clearAppDataLIF(ps.getPkg(), UserHandle.USER_ALL,
+ FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
+ | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+ }
+ } catch (PackageManagerException e) {
+ request.setError(e.error, e.getMessage());
+ return;
+ }
+ DexOptHelper.performDexoptIfNeeded(request, mDexManager, mContext, null);
+ }
+ }
+
private boolean renameAndUpdatePaths(List<InstallRequest> requests) {
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
for (InstallRequest request : requests) {
@@ -2655,20 +2729,22 @@
incrementalStorages.add(storage);
}
- // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088)
- mAppDataHelper.prepareAppDataPostCommitLIF(ps, 0, installRequest.getNewUsers());
- if (installRequest.isClearCodeCache()) {
- mAppDataHelper.clearAppDataLIF(ps.getPkg(), UserHandle.USER_ALL,
- FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
- | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
- }
if (installRequest.isInstallReplace() && pkg != null) {
mDexManager.notifyPackageUpdated(packageName,
pkg.getBaseApkPath(), pkg.getSplitCodePaths());
}
+ if (!Flags.improveInstallFreeze()) {
+ // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088)
+ mAppDataHelper.prepareAppDataPostCommitLIF(ps, 0, installRequest.getNewUsers());
+ if (installRequest.isClearCodeCache()) {
+ mAppDataHelper.clearAppDataLIF(ps.getPkg(), UserHandle.USER_ALL,
+ FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
+ | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+ }
- DexOptHelper.performDexoptIfNeeded(installRequest, mDexManager, mContext,
- mPm.mInstallLock.getRawLock());
+ DexOptHelper.performDexoptIfNeeded(installRequest, mDexManager, mContext,
+ mPm.mInstallLock.getRawLock());
+ }
}
PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
incrementalStorages);
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 8657de2..5653da0 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -716,7 +716,7 @@
visiblePackages.add(info.getActivityInfo().packageName);
}
final List<ApplicationInfo> installedPackages =
- mPackageManagerInternal.getInstalledApplicationsCrossUser(
+ mPackageManagerInternal.getInstalledApplications(
/* flags= */ 0, user.getIdentifier(), callingUid);
for (ApplicationInfo applicationInfo : installedPackages) {
if (!visiblePackages.contains(applicationInfo.packageName)) {
diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
index cf5de89..a28e3c1 100644
--- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
@@ -244,7 +244,7 @@
return;
}
int registerUid = registerUser.getUid();
- if (allowUids != null && registerUid != Process.SYSTEM_UID
+ if (allowUids != null && !UserHandle.isSameApp(registerUid, Process.SYSTEM_UID)
&& !ArrayUtils.contains(allowUids, registerUid)) {
if (DEBUG) {
Slog.w(TAG, "Skip invoke PackageMonitorCallback for " + intent.getAction()
diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING
index 21a6df2..94b49e5 100644
--- a/services/core/java/com/android/server/pm/TEST_MAPPING
+++ b/services/core/java/com/android/server/pm/TEST_MAPPING
@@ -101,6 +101,22 @@
]
},
{
+ "name": "CtsPackageInstallerCUJDeviceAdminTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJInstallationTestCases",
"file_patterns": [
"core/java/.*Install.*",
@@ -117,6 +133,22 @@
]
},
{
+ "name": "CtsPackageInstallerCUJMultiUsersTestCases",
+ "file_patterns": [
+ "core/java/.*Install.*",
+ "services/core/.*Install.*",
+ "services/core/java/com/android/server/pm/.*"
+ ],
+ "options":[
+ {
+ "exclude-annotation":"androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation":"org.junit.Ignore"
+ }
+ ]
+ },
+ {
"name": "CtsPackageInstallerCUJUninstallationTestCases",
"file_patterns": [
"core/java/.*Install.*",
diff --git a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
index 55afb17..c22e382 100644
--- a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
+++ b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java
@@ -31,6 +31,7 @@
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.pkg.SharedUserApi;
import com.android.server.pm.snapshot.PackageDataSnapshot;
@@ -71,8 +72,26 @@
@NonNull
@Override
public FilteredSnapshotImpl withFilteredSnapshot(int callingUid, @NonNull UserHandle user) {
+ return withFilteredSnapshot(callingUid, user, /* uncommittedPs= */ null);
+ }
+
+ /**
+ * Creates a {@link FilteredSnapshot} with a uncommitted {@link PackageState} that is used for
+ * dexopt in the art service to get the correct package state before the package is committed.
+ */
+ @NonNull
+ public static FilteredSnapshotImpl withFilteredSnapshot(PackageManagerLocal pm,
+ @NonNull PackageState uncommittedPs) {
+ return ((PackageManagerLocalImpl) pm).withFilteredSnapshot(Binder.getCallingUid(),
+ Binder.getCallingUserHandle(), uncommittedPs);
+ }
+
+ @NonNull
+ private FilteredSnapshotImpl withFilteredSnapshot(int callingUid, @NonNull UserHandle user,
+ @Nullable PackageState uncommittedPs) {
return new FilteredSnapshotImpl(callingUid, user,
- mService.snapshotComputer(false /*allowLiveComputer*/), null);
+ mService.snapshotComputer(/* allowLiveComputer= */ false),
+ /* parentSnapshot= */ null, uncommittedPs);
}
@Override
@@ -145,7 +164,8 @@
@Override
public FilteredSnapshot filtered(int callingUid, @NonNull UserHandle user) {
- return new FilteredSnapshotImpl(callingUid, user, mSnapshot, this);
+ return new FilteredSnapshotImpl(callingUid, user, mSnapshot, this,
+ /* uncommittedPs= */ null);
}
@SuppressWarnings("RedundantSuppression")
@@ -209,13 +229,18 @@
@Nullable
private final UnfilteredSnapshotImpl mParentSnapshot;
+ @Nullable
+ private final PackageState mUncommitPackageState;
+
private FilteredSnapshotImpl(int callingUid, @NonNull UserHandle user,
@NonNull PackageDataSnapshot snapshot,
- @Nullable UnfilteredSnapshotImpl parentSnapshot) {
+ @Nullable UnfilteredSnapshotImpl parentSnapshot,
+ @Nullable PackageState uncommittedPs) {
super(snapshot);
mCallingUid = callingUid;
mUserId = user.getIdentifier();
mParentSnapshot = parentSnapshot;
+ mUncommitPackageState = uncommittedPs;
}
@Override
@@ -237,6 +262,10 @@
@Override
public PackageState getPackageState(@NonNull String packageName) {
checkClosed();
+ if (mUncommitPackageState != null
+ && packageName.equals(mUncommitPackageState.getPackageName())) {
+ return mUncommitPackageState;
+ }
return mSnapshot.getPackageStateFiltered(packageName, mCallingUid, mUserId);
}
@@ -250,6 +279,11 @@
var filteredPackageStates = new ArrayMap<String, PackageState>();
for (int index = 0, size = packageStates.size(); index < size; index++) {
var packageState = packageStates.valueAt(index);
+ if (mUncommitPackageState != null
+ && packageState.getPackageName().equals(
+ mUncommitPackageState.getPackageName())) {
+ packageState = (PackageStateInternal) mUncommitPackageState;
+ }
if (!mSnapshot.shouldFilterApplication(packageState, mCallingUid, mUserId)) {
filteredPackageStates.put(packageStates.keyAt(index), packageState);
}
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 6e7a009..bc6a40a 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -884,10 +884,13 @@
}
// Allow voice search on wear
- grantPermissionsToSystemPackage(pm,
- getDefaultSystemHandlerActivityPackage(pm,
- SearchManager.INTENT_ACTION_GLOBAL_SEARCH, userId),
- userId, PHONE_PERMISSIONS, CALENDAR_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS);
+ String voiceSearchPackage = getDefaultSystemHandlerActivityPackage(pm,
+ SearchManager.INTENT_ACTION_GLOBAL_SEARCH, userId);
+ grantPermissionsToSystemPackage(pm, voiceSearchPackage,
+ userId, PHONE_PERMISSIONS, CALENDAR_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS,
+ COARSE_BACKGROUND_LOCATION_PERMISSIONS);
+ revokeRuntimePermissions(pm, voiceSearchPackage,
+ FINE_LOCATION_PERMISSIONS, false, userId);
}
// Print Spooler
diff --git a/services/core/java/com/android/server/policy/ModifierShortcutManager.java b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
index 027e69c..66ec53e 100644
--- a/services/core/java/com/android/server/policy/ModifierShortcutManager.java
+++ b/services/core/java/com/android/server/policy/ModifierShortcutManager.java
@@ -667,9 +667,11 @@
public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
List<KeyboardShortcutInfo> shortcuts = new ArrayList();
if (modifierShortcutManagerRefactor()) {
+ Context context = modifierShortcutManagerMultiuser()
+ ? mContext.createContextAsUser(mCurrentUser, 0) : mContext;
for (Bookmark b : mBookmarks.values()) {
KeyboardShortcutInfo info = shortcutInfoFromIntent(
- b.getShortcutChar(), b.getIntent(mContext), b.isShift());
+ b.getShortcutChar(), b.getIntent(context), b.isShift());
if (info != null) {
shortcuts.add(info);
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 2284050..e47b4c2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -373,6 +373,7 @@
//The config value can be overridden using Settings.Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS
static final int DOUBLE_PRESS_PRIMARY_NOTHING = 0;
static final int DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP = 1;
+ static final int DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP = 2;
// Must match: config_triplePressOnStemPrimaryBehavior in config.xml
// The config value can be overridden using Settings.Global.STEM_PRIMARY_BUTTON_TRIPLE_PRESS
@@ -1596,6 +1597,12 @@
performStemPrimaryDoublePressSwitchToRecentTask();
}
break;
+ case DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP:
+ final int stemPrimaryKeyDeviceId = INVALID_INPUT_DEVICE_ID;
+ handleKeyGestureInKeyGestureController(
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS,
+ stemPrimaryKeyDeviceId, KEYCODE_STEM_PRIMARY, /* metaState= */ 0);
+ break;
}
}
@@ -3204,8 +3211,9 @@
return ADD_OKAY;
}
- // Allow virtual device owners to add overlays on the displays they own.
+ // Allow virtual device owners to add overlays on the trusted displays they own.
if (mWindowManagerFuncs.isCallerVirtualDeviceOwner(displayId, callingUid)
+ && mWindowManagerFuncs.isDisplayTrusted(displayId)
&& mContext.checkCallingOrSelfPermission(CREATE_VIRTUAL_DEVICE)
== PERMISSION_GRANTED) {
return ADD_OKAY;
@@ -7243,6 +7251,8 @@
return "DOUBLE_PRESS_PRIMARY_NOTHING";
case DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP:
return "DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP";
+ case DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP:
+ return "DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP";
default:
return Integer.toString(behavior);
}
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index ad11657..cc31bb1 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -368,6 +368,11 @@
* belongs to.
*/
boolean isCallerVirtualDeviceOwner(int displayId, int callingUid);
+
+ /**
+ * Returns whether the display with the given ID is trusted.
+ */
+ boolean isDisplayTrusted(int displayId);
}
/**
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 0cdf537..4fae798 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -164,6 +164,7 @@
}
private final SparseArray<Interactivity> mInteractivityByGroupId = new SparseArray<>();
+ private SparseBooleanArray mDisplayInteractivities = new SparseBooleanArray();
// The current global interactive state. This is set as soon as an interactive state
// transition begins so as to capture the reason that it happened. At some point
@@ -690,6 +691,42 @@
}
/**
+ * Update the interactivities of the displays in given DisplayGroup.
+ *
+ * @param groupId The group id of the DisplayGroup to update display interactivities for.
+ */
+ private void updateDisplayInteractivities(int groupId, boolean interactive) {
+ final int[] displayIds = mDisplayManagerInternal.getDisplayIdsForGroup(groupId);
+ for (int displayId : displayIds) {
+ mDisplayInteractivities.put(displayId, interactive);
+ }
+
+ }
+
+ private void resetDisplayInteractivities() {
+ final SparseArray<int[]> displaysByGroupId =
+ mDisplayManagerInternal.getDisplayIdsByGroupsIds();
+ SparseBooleanArray newDisplayInteractivities = new SparseBooleanArray();
+ for (int i = 0; i < displaysByGroupId.size(); i++) {
+ final int groupId = displaysByGroupId.keyAt(i);
+ for (int displayId : displaysByGroupId.get(i)) {
+ // If we already know display interactivity, use that
+ if (mDisplayInteractivities.indexOfKey(displayId) > 0) {
+ newDisplayInteractivities.put(
+ displayId, mDisplayInteractivities.get(displayId));
+ } else { // If display is new to Notifier, use the power group's interactive value
+ final Interactivity groupInteractivity = mInteractivityByGroupId.get(groupId);
+ // If group Interactivity hasn't been initialized, assume group is interactive
+ final boolean groupInteractive =
+ groupInteractivity == null || groupInteractivity.isInteractive;
+ newDisplayInteractivities.put(displayId, groupInteractive);
+ }
+ }
+ }
+ mDisplayInteractivities = newDisplayInteractivities;
+ }
+
+ /**
* Called when an individual PowerGroup changes wakefulness.
*/
public void onGroupWakefulnessChangeStarted(int groupId, int wakefulness, int changeReason,
@@ -717,6 +754,12 @@
handleEarlyInteractiveChange(groupId);
mWakefulnessSessionObserver.onWakefulnessChangeStarted(groupId, wakefulness,
changeReason, eventTime);
+
+ // Update input on which displays are interactive
+ if (mFlags.isPerDisplayWakeByTouchEnabled()) {
+ updateDisplayInteractivities(groupId, isInteractive);
+ mInputManagerInternal.setDisplayInteractivities(mDisplayInteractivities);
+ }
}
}
@@ -731,6 +774,16 @@
}
/**
+ * Called when a PowerGroup has been changed.
+ */
+ public void onGroupChanged() {
+ if (mFlags.isPerDisplayWakeByTouchEnabled()) {
+ resetDisplayInteractivities();
+ mInputManagerInternal.setDisplayInteractivities(mDisplayInteractivities);
+ }
+ }
+
+ /**
* Called when there has been user activity.
*/
public void onUserActivity(int displayGroupId, @PowerManager.UserActivityEvent int event,
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index e053964..21ab781 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -125,7 +125,6 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.util.Preconditions;
-import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.EventLogTags;
import com.android.server.LockGuard;
import com.android.server.ServiceThread;
@@ -133,6 +132,7 @@
import com.android.server.UiThread;
import com.android.server.Watchdog;
import com.android.server.am.BatteryStatsService;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.display.feature.DeviceConfigParameterProvider;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
@@ -2445,6 +2445,8 @@
mClock.uptimeMillis());
} else if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_REMOVED) {
mNotifier.onGroupRemoved(groupId);
+ } else if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_CHANGED) {
+ mNotifier.onGroupChanged();
}
if (oldWakefulness != newWakefulness) {
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 1346a29..dc48242 100644
--- a/services/core/java/com/android/server/power/hint/HintManagerService.java
+++ b/services/core/java/com/android/server/power/hint/HintManagerService.java
@@ -1282,11 +1282,9 @@
boolean updateHintAllowedByProcState(boolean allowed) {
synchronized (this) {
if (allowed && !mUpdateAllowedByProcState && !mShouldForcePause) {
- Slogf.e(TAG, "ADPF IS GETTING RESUMED? UID: " + mUid + " TAG: " + mTag);
resume();
}
if (!allowed && mUpdateAllowedByProcState) {
- Slogf.e(TAG, "ADPF IS GETTING PAUSED? UID: " + mUid + " TAG: " + mTag);
pause();
}
mUpdateAllowedByProcState = allowed;
diff --git a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java
index 539c415..d7aa987 100644
--- a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsCollector.java
@@ -30,6 +30,7 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.BluetoothPowerStatsLayout;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@@ -142,6 +143,7 @@
return null;
}
+ Arrays.fill(mDeviceStats, 0);
mPowerStats.uidStats.clear();
collectBluetoothActivityInfo();
diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
index 128f14a..dd6484d 100644
--- a/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsCollector.java
@@ -34,6 +34,7 @@
import com.android.server.power.stats.format.CpuPowerStatsLayout;
import java.io.PrintWriter;
+import java.util.Arrays;
import java.util.Locale;
/**
@@ -330,7 +331,9 @@
return null;
}
+ Arrays.fill(mCpuPowerStats.stats, 0);
mCpuPowerStats.uidStats.clear();
+
// TODO(b/305120724): additionally retrieve time-in-cluster for each CPU cluster
long newTimestampNanos = mKernelCpuStatsReader.readCpuStats(this::processUidStats,
mLayout.getScalingStepToPowerBracketMap(), mLastUpdateTimestampNanos,
diff --git a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
index 1d2e388..079fc3c 100644
--- a/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/EnergyConsumerPowerStatsCollector.java
@@ -24,6 +24,8 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.EnergyConsumerPowerStatsLayout;
+import java.util.Arrays;
+
public class EnergyConsumerPowerStatsCollector extends PowerStatsCollector {
public interface Injector {
@@ -105,6 +107,7 @@
return null;
}
+ Arrays.fill(mPowerStats.stats, 0);
mPowerStats.uidStats.clear();
if (!mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout)) {
diff --git a/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java
index 8371e66..c38904f 100644
--- a/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/ScreenPowerStatsCollector.java
@@ -27,6 +27,8 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.ScreenPowerStatsLayout;
+import java.util.Arrays;
+
public class ScreenPowerStatsCollector extends PowerStatsCollector {
private static final String TAG = "ScreenPowerStatsCollector";
@@ -115,6 +117,9 @@
return null;
}
+ Arrays.fill(mPowerStats.stats, 0);
+ mPowerStats.uidStats.clear();
+
mConsumedEnergyHelper.collectConsumedEnergy(mPowerStats, mLayout);
for (int display = 0; display < mDisplayCount; display++) {
@@ -142,8 +147,6 @@
mLastDozeTime[display] = screenDozeTimeMs;
}
- mPowerStats.uidStats.clear();
-
mScreenUsageTimeRetriever.retrieveTopActivityTimes((uid, topActivityTimeMs) -> {
long topActivityDuration = topActivityTimeMs - mLastTopActivityTime.get(uid);
if (topActivityDuration == 0) {
diff --git a/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java
index 9e4a391..e36c994 100644
--- a/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WakelockPowerStatsCollector.java
@@ -25,6 +25,8 @@
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.WakelockPowerStatsLayout;
+import java.util.Arrays;
+
class WakelockPowerStatsCollector extends PowerStatsCollector {
public interface WakelockDurationRetriever {
@@ -89,6 +91,9 @@
return null;
}
+ Arrays.fill(mPowerStats.stats, 0);
+ mPowerStats.uidStats.clear();
+
long elapsedRealtime = mClock.elapsedRealtime();
mPowerStats.durationMs = elapsedRealtime - mLastCollectionTime;
@@ -101,7 +106,6 @@
mLastWakelockDurationMs = wakelockDurationMillis;
- mPowerStats.uidStats.clear();
mWakelockDurationRetriever.retrieveUidWakelockDuration((uid, durationMs) -> {
if (!mFirstCollection) {
long[] uidStats = mPowerStats.uidStats.get(uid);
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 7a84b05..1fdeac9 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java
@@ -30,6 +30,7 @@
import com.android.internal.os.PowerStats;
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;
@@ -151,6 +152,9 @@
return null;
}
+ Arrays.fill(mDeviceStats, 0);
+ mPowerStats.uidStats.clear();
+
WifiActivityEnergyInfo activityInfo = null;
if (mPowerReportingSupported) {
activityInfo = collectWifiActivityInfo();
@@ -224,8 +228,6 @@
}
private List<BatteryStatsImpl.NetworkStatsDelta> collectNetworkStats() {
- mPowerStats.uidStats.clear();
-
NetworkStats networkStats = mNetworkStatsSupplier.get();
if (networkStats == null) {
return null;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5372abe..2972771 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -230,6 +230,7 @@
import static com.android.server.wm.IdentifierProto.TITLE;
import static com.android.server.wm.IdentifierProto.USER_ID;
import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
@@ -286,6 +287,9 @@
import android.app.servertransaction.TopResumedActivityChangeItem;
import android.app.servertransaction.TransferSplashScreenViewStateItem;
import android.app.usage.UsageEvents.Event;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.Overridable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -465,6 +469,11 @@
// finished destroying itself.
private static final int DESTROY_TIMEOUT = 10 * 1000;
+ @ChangeId
+ @Overridable
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ static final long UNIVERSAL_RESIZABLE_BY_DEFAULT = 357141415;
+
final ActivityTaskManagerService mAtmService;
final ActivityCallerState mCallerState;
@NonNull
@@ -2635,8 +2644,10 @@
if (finishing || !mHandleExitSplashScreen || mStartingSurface == null
|| mStartingWindow == null
|| mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_FINISH
- // skip copy splash screen to client if it was resized
- || (mStartingData != null && mStartingData.mResizedFromTransfer)
+ // Skip copy splash screen to client if it was resized, or the starting data already
+ // requested to be removed after transaction commit.
+ || (mStartingData != null && (mStartingData.mResizedFromTransfer
+ || mStartingData.mRemoveAfterTransaction != AFTER_TRANSACTION_IDLE))
|| isRelaunching()) {
return false;
}
@@ -3179,11 +3190,20 @@
* will be ignored.
*/
boolean isUniversalResizeable() {
- return mWmService.mConstants.mIgnoreActivityOrientationRequest
- && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME
- // If the user preference respects aspect ratio, then it becomes non-resizable.
- && !mAppCompatController.getAppCompatOverrides().getAppCompatAspectRatioOverrides()
- .shouldApplyUserMinAspectRatioOverride();
+ if (info.applicationInfo.category == ApplicationInfo.CATEGORY_GAME) {
+ return false;
+ }
+ final boolean compatEnabled = Flags.universalResizableByDefault()
+ && mDisplayContent != null && mDisplayContent.getConfiguration()
+ .smallestScreenWidthDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ && mDisplayContent.getIgnoreOrientationRequest()
+ && info.isChangeEnabled(UNIVERSAL_RESIZABLE_BY_DEFAULT);
+ if (!compatEnabled && !mWmService.mConstants.mIgnoreActivityOrientationRequest) {
+ return false;
+ }
+ // If the user preference respects aspect ratio, then it becomes non-resizable.
+ return !mAppCompatController.getAppCompatOverrides().getAppCompatAspectRatioOverrides()
+ .shouldApplyUserMinAspectRatioOverride();
}
boolean isResizeable() {
@@ -8179,7 +8199,7 @@
@ActivityInfo.ScreenOrientation
protected int getOverrideOrientation() {
int candidateOrientation = super.getOverrideOrientation();
- if (isUniversalResizeable() && ActivityInfo.isFixedOrientation(candidateOrientation)) {
+ if (ActivityInfo.isFixedOrientation(candidateOrientation) && isUniversalResizeable()) {
candidateOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
}
return mAppCompatController.getOrientationPolicy()
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 49380d4..87fa62a 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2759,10 +2759,7 @@
mInTask = null;
// Launch ResolverActivity in the source task, so that it stays in the task bounds
// when in freeform workspace.
- // Also put noDisplay activities in the source task. These by itself can be placed
- // in any task/root-task, however it could launch other activities like
- // ResolverActivity, and we want those to stay in the original task.
- if ((mStartActivity.isResolverOrDelegateActivity() || mStartActivity.noDisplay)
+ if (mStartActivity.isResolverOrDelegateActivity()
&& mSourceRecord != null && mSourceRecord.inFreeformWindowingMode()) {
mAddingToTask = true;
}
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index a74b006..4824c16 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -21,6 +21,7 @@
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.graphics.Rect;
import android.util.Log;
import android.view.Surface;
@@ -128,7 +129,7 @@
/**
* Set the parameters to prepare the dim to be relative parented to the dimming container
*/
- void prepareReparent(@NonNull WindowContainer<?> geometryParent,
+ void prepareReparent(@Nullable WindowContainer<?> geometryParent,
@NonNull WindowState relativeParent) {
mAnimationHelper.setRequestedRelativeParent(relativeParent);
mAnimationHelper.setRequestedGeometryParent(geometryParent);
@@ -221,7 +222,7 @@
* @param dimmingContainer The container that is dimming. The dim layer will be rel-z
* parented below it
*/
- public void adjustPosition(@NonNull WindowContainer<?> geometryParent,
+ public void adjustPosition(@Nullable WindowContainer<?> geometryParent,
@NonNull WindowState dimmingContainer) {
if (mDimState != null) {
mDimState.prepareReparent(geometryParent, dimmingContainer);
diff --git a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
index 298edae..3999e03 100644
--- a/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
+++ b/services/core/java/com/android/server/wm/DimmerAnimationHelper.java
@@ -108,7 +108,7 @@
}
// Sets the requested layer to reparent the dim to without applying it immediately
- void setRequestedGeometryParent(WindowContainer<?> geometryParent) {
+ void setRequestedGeometryParent(@Nullable WindowContainer<?> geometryParent) {
if (geometryParent != null) {
mRequestedProperties.mGeometryParent = geometryParent;
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 52e8285..c062f5a 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1073,7 +1073,8 @@
final String systemUiPermission =
mService.isCallerVirtualDeviceOwner(mDisplayContent.getDisplayId(), callingUid)
- // Allow virtual device owners to add system windows on their displays.
+ && mDisplayContent.isTrusted()
+ // Virtual device owners can add system windows on their trusted displays.
? android.Manifest.permission.CREATE_VIRTUAL_DEVICE
: android.Manifest.permission.STATUS_BAR_SERVICE;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c48590f..188b368 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1947,7 +1947,8 @@
mCleanupTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
buildCleanupTransaction(mCleanupTransaction, info);
if (mController.getTransitionPlayer() != null && mIsPlayerEnabled) {
- mController.dispatchLegacyAppTransitionStarting(info, mStatusBarTransitionDelay);
+ mController.dispatchLegacyAppTransitionStarting(participantDisplays,
+ mStatusBarTransitionDelay);
try {
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS,
"Calling onTransitionReady: %s", info);
@@ -2188,32 +2189,34 @@
for (int i = onTopTasksEnd.size() - 1; i >= 0; --i) {
final Task task = onTopTasksEnd.get(i);
if (task.getDisplayId() != displayId) continue;
- if (!enableDisplayFocusInShellTransitions()
- || mOnTopDisplayStart == onTopDisplayEnd
- || displayId != onTopDisplayEnd.mDisplayId) {
- // If it didn't change since last report, don't report
- if (reportedOnTop == null) {
- if (mOnTopTasksStart.contains(task)) continue;
- } else if (reportedOnTop.contains(task)) {
- continue;
- }
+ if (reportedOnTop == null) {
+ if (mOnTopTasksStart.contains(task)) continue;
+ } else if (reportedOnTop.contains(task)) {
+ continue;
}
- // Need to report it.
- mParticipants.add(task);
- int changeIdx = mChanges.indexOfKey(task);
- if (changeIdx < 0) {
- mChanges.put(task, new ChangeInfo(task));
- changeIdx = mChanges.indexOfKey(task);
- }
- mChanges.valueAt(changeIdx).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP;
+ addToTopChange(task);
}
// Swap in the latest on-top tasks.
mController.mLatestOnTopTasksReported.put(displayId, onTopTasksEnd);
onTopTasksEnd = reportedOnTop != null ? reportedOnTop : new ArrayList<>();
onTopTasksEnd.clear();
+
+ if (enableDisplayFocusInShellTransitions()
+ && mOnTopDisplayStart != onTopDisplayEnd
+ && displayId == onTopDisplayEnd.mDisplayId) {
+ addToTopChange(onTopDisplayEnd);
+ }
}
}
+ private void addToTopChange(@NonNull WindowContainer wc) {
+ mParticipants.add(wc);
+ if (!mChanges.containsKey(wc)) {
+ mChanges.put(wc, new ChangeInfo(wc));
+ }
+ mChanges.get(wc).mFlags |= ChangeInfo.FLAG_CHANGE_MOVED_TO_TOP;
+ }
+
private void postCleanupOnFailure() {
mController.mAtm.mH.post(() -> {
synchronized (mController.mAtm.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 37c3ce80..b7fe327 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -1356,12 +1356,13 @@
}
}
- void dispatchLegacyAppTransitionStarting(TransitionInfo info, long statusBarTransitionDelay) {
+ void dispatchLegacyAppTransitionStarting(DisplayContent[] participantDisplays,
+ long statusBarTransitionDelay) {
final long now = SystemClock.uptimeMillis();
for (int i = 0; i < mLegacyListeners.size(); ++i) {
final WindowManagerInternal.AppTransitionListener listener = mLegacyListeners.get(i);
- for (int j = 0; j < info.getRootCount(); ++j) {
- final int displayId = info.getRoot(j).getDisplayId();
+ for (int j = 0; j < participantDisplays.length; ++j) {
+ final int displayId = participantDisplays[j].mDisplayId;
if (shouldDispatchLegacyListener(listener, displayId)) {
listener.onAppTransitionStartingLocked(
now + statusBarTransitionDelay,
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 7925220..e1e4714 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -10172,6 +10172,22 @@
}
}
+ /**
+ * Returns whether the display with the given ID is trusted.
+ */
+ @Override
+ public boolean isDisplayTrusted(int displayId) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ DisplayContent dc = mRoot.getDisplayContent(displayId);
+ return dc != null && dc.isTrusted();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
@RequiresPermission(ACCESS_SURFACE_FLINGER)
@Override
public boolean replaceContentOnDisplay(int displayId, SurfaceControl sc) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 6d7396f..21ed8d7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -16,6 +16,9 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.os.Build.IS_USER;
import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED;
@@ -30,6 +33,7 @@
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import android.app.WindowConfiguration;
import android.content.res.Resources.NotFoundException;
import android.graphics.Color;
import android.graphics.Point;
@@ -157,6 +161,10 @@
return runReset(pw);
case "disable-blur":
return runSetBlurDisabled(pw);
+ case "set-display-windowing-mode":
+ return runSetDisplayWindowingMode(pw);
+ case "get-display-windowing-mode":
+ return runGetDisplayWindowingMode(pw);
case "shell":
return runWmShellCommand(pw);
default:
@@ -1434,6 +1442,35 @@
return 0;
}
+ private int runSetDisplayWindowingMode(PrintWriter pw) throws RemoteException {
+ int displayId = Display.DEFAULT_DISPLAY;
+ String arg = getNextArgRequired();
+ if ("-d".equals(arg)) {
+ displayId = Integer.parseInt(getNextArgRequired());
+ arg = getNextArgRequired();
+ }
+
+ final int windowingMode = Integer.parseInt(arg);
+ mInterface.setWindowingMode(displayId, windowingMode);
+
+ return 0;
+ }
+
+ private int runGetDisplayWindowingMode(PrintWriter pw) throws RemoteException {
+ int displayId = Display.DEFAULT_DISPLAY;
+ final String arg = getNextArg();
+ if ("-d".equals(arg)) {
+ displayId = Integer.parseInt(getNextArgRequired());
+ }
+
+ final int windowingMode = mInterface.getWindowingMode(displayId);
+ pw.println("display windowing mode="
+ + WindowConfiguration.windowingModeToString(windowingMode) + " for displayId="
+ + displayId);
+
+ return 0;
+ }
+
private int runWmShellCommand(PrintWriter pw) {
String arg = getNextArg();
@@ -1512,6 +1549,9 @@
// set-multi-window-config
runResetMultiWindowConfig();
+ // set-display-windowing-mode
+ mInternal.setWindowingMode(displayId, WINDOWING_MODE_UNDEFINED);
+
pw.println("Reset all settings for displayId=" + displayId);
return 0;
}
@@ -1552,6 +1592,12 @@
printLetterboxHelp(pw);
printMultiWindowConfigHelp(pw);
+ pw.println(" set-display-windowing-mode [-d DISPLAY_ID] [mode_id]");
+ pw.println(" As mode_id, use " + WINDOWING_MODE_UNDEFINED + " for undefined, "
+ + WINDOWING_MODE_FREEFORM + " for freeform, " + WINDOWING_MODE_FULLSCREEN + " for"
+ + " fullscreen");
+ pw.println(" get-display-windowing-mode [-d DISPLAY_ID]");
+
pw.println(" reset [-d DISPLAY_ID]");
pw.println(" Reset all override settings.");
if (!IS_USER) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0878912..66f9230 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5215,7 +5215,7 @@
// but not window manager visible (!isVisibleNow()), it can still be the parent of the
// dim, but can not create a new surface or continue a dim alone.
Dimmer dimmer;
- WindowContainer<?> geometryParent = task;
+ WindowContainer<?> geometryParent = null;
if (Flags.useTasksDimOnly()) {
geometryParent = getDimParent();
dimmer = getDimController();
diff --git a/services/profcollect/src/com/android/server/profcollect/Utils.java b/services/profcollect/src/com/android/server/profcollect/Utils.java
index b4e2544..a8016a0 100644
--- a/services/profcollect/src/com/android/server/profcollect/Utils.java
+++ b/services/profcollect/src/com/android/server/profcollect/Utils.java
@@ -25,10 +25,14 @@
import com.android.internal.os.BackgroundThread;
+import java.time.Instant;
import java.util.concurrent.ThreadLocalRandom;
public final class Utils {
+ private static Instant lastTraceTime = Instant.EPOCH;
+ private static final int TRACE_COOLDOWN_SECONDS = 30;
+
public static boolean withFrequency(String configName, int defaultFrequency) {
int threshold = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, configName, defaultFrequency);
@@ -40,6 +44,9 @@
if (mIProfcollect == null) {
return false;
}
+ if (isInCooldownOrReset()) {
+ return false;
+ }
BackgroundThread.get().getThreadHandler().post(() -> {
try {
mIProfcollect.trace_system(eventName);
@@ -54,6 +61,9 @@
if (mIProfcollect == null) {
return false;
}
+ if (isInCooldownOrReset()) {
+ return false;
+ }
BackgroundThread.get().getThreadHandler().postDelayed(() -> {
try {
mIProfcollect.trace_system(eventName);
@@ -69,6 +79,9 @@
if (mIProfcollect == null) {
return false;
}
+ if (isInCooldownOrReset()) {
+ return false;
+ }
BackgroundThread.get().getThreadHandler().post(() -> {
try {
mIProfcollect.trace_process(eventName,
@@ -80,4 +93,16 @@
});
return true;
}
+
+ /**
+ * Returns true if the last trace is within the cooldown period. If the last trace is outside
+ * the cooldown period, the last trace time is reset to the current time.
+ */
+ private static boolean isInCooldownOrReset() {
+ if (!Instant.now().isBefore(lastTraceTime.plusSeconds(TRACE_COOLDOWN_SECONDS))) {
+ lastTraceTime = Instant.now();
+ return false;
+ }
+ return true;
+ }
}
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 7d5532f..5c4716d 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,7 +57,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doReturn
@@ -384,10 +383,6 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
PackageManager.PERMISSION_GRANTED
}
- whenever(this.checkPermission(
- eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
- PackageManager.PERMISSION_GRANTED
- }
}
val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 255dcb0..6093a67 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -37,6 +37,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -1308,6 +1309,38 @@
}
/**
+ * Tests that it's not allowed to create an auto-mirror virtual display without
+ * CAPTURE_VIDEO_OUTPUT permission or a virtual device that can mirror displays
+ */
+ @Test
+ public void createAutoMirrorDisplay_withoutPermissionOrAllowedVirtualDevice_throwsException()
+ throws Exception {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ registerDefaultDisplays(displayManager);
+ when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+ IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+ when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(virtualDevice.canCreateMirrorDisplays()).thenReturn(false);
+ when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+ when(mContext.checkCallingPermission(CAPTURE_VIDEO_OUTPUT)).thenReturn(
+ PackageManager.PERMISSION_DENIED);
+
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR)
+ .setUniqueId("uniqueId --- mirror display");
+ assertThrows(SecurityException.class, () -> {
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+ });
+ }
+
+ /**
* Tests that the virtual display is added to the default display group when created with
* VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device.
*/
@@ -1319,6 +1352,7 @@
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(virtualDevice.canCreateMirrorDisplays()).thenReturn(true);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
// Create an auto-mirror virtual display using a virtual device.
@@ -1351,6 +1385,7 @@
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(virtualDevice.canCreateMirrorDisplays()).thenReturn(true);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
// Create an auto-mirror virtual display using a virtual device.
@@ -1417,6 +1452,7 @@
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(virtualDevice.canCreateMirrorDisplays()).thenReturn(true);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
when(mContext.checkCallingPermission(ADD_ALWAYS_UNLOCKED_DISPLAY))
.thenReturn(PackageManager.PERMISSION_GRANTED);
@@ -1452,6 +1488,7 @@
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(virtualDevice.canCreateMirrorDisplays()).thenReturn(true);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
// Create an auto-mirror virtual display using a virtual device.
@@ -1524,6 +1561,47 @@
}
@Test
+ public void testGetDisplayIdsForGroup() throws Exception {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ // Create display 1
+ FakeDisplayDevice displayDevice1 =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+ LogicalDisplay display1 = logicalDisplayMapper.getDisplayLocked(displayDevice1);
+ final int groupId1 = display1.getDisplayInfoLocked().displayGroupId;
+ // Create display 2
+ FakeDisplayDevice displayDevice2 =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+ LogicalDisplay display2 = logicalDisplayMapper.getDisplayLocked(displayDevice2);
+ final int groupId2 = display2.getDisplayInfoLocked().displayGroupId;
+ // Both displays should be in the same display group
+ assertEquals(groupId1, groupId2);
+ final int[] expectedDisplayIds = new int[]{
+ display1.getDisplayIdLocked(), display2.getDisplayIdLocked()};
+
+ final int[] displayIds = localService.getDisplayIdsForGroup(groupId1);
+
+ assertArrayEquals(expectedDisplayIds, displayIds);
+ }
+
+ @Test
+ public void testGetDisplayIdsForUnknownGroup() throws Exception {
+ final int unknownDisplayGroupId = 999;
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ // Verify that display manager does not have display group
+ assertNull(logicalDisplayMapper.getDisplayGroupLocked(unknownDisplayGroupId));
+
+ final int[] displayIds = localService.getDisplayIdsForGroup(unknownDisplayGroupId);
+
+ assertEquals(0, displayIds.length);
+ }
+
+ @Test
public void testCreateVirtualDisplay_isValidProjection_notValid()
throws RemoteException {
when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
index e863f15..e678acc 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java
@@ -39,6 +39,7 @@
import android.os.FileUtils;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.Parcel;
import android.os.Process;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
@@ -580,6 +581,50 @@
assertTrue(mAppStartInfoTracker.mMonotonicClock.monotonicTime() >= originalMonotonicTime);
}
+ /**
+ * Test to confirm that parcel read and write implementations match, correctly loading records
+ * with the same values and leaving no data unread.
+ */
+ @Test
+ public void testParcelReadWriteMatch() throws Exception {
+ // Create a start info records with all fields set.
+ ApplicationStartInfo startInfo = new ApplicationStartInfo(1234L);
+ startInfo.setPid(123);
+ startInfo.setRealUid(987);
+ startInfo.setPackageUid(654);
+ startInfo.setDefiningUid(321);
+ startInfo.setReason(ApplicationStartInfo.START_REASON_LAUNCHER);
+ startInfo.setStartupState(ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN);
+ startInfo.setStartType(ApplicationStartInfo.START_TYPE_WARM);
+ startInfo.setLaunchMode(ApplicationStartInfo.LAUNCH_MODE_SINGLE_TOP);
+ startInfo.setPackageName(APP_1_PACKAGE_NAME);
+ startInfo.setProcessName(APP_1_PROCESS_NAME);
+ startInfo.addStartupTimestamp(ApplicationStartInfo.START_TIMESTAMP_LAUNCH, 999L);
+ startInfo.addStartupTimestamp(ApplicationStartInfo.START_TIMESTAMP_FIRST_FRAME, 888L);
+ startInfo.setForceStopped(true);
+ startInfo.setStartComponent(ApplicationStartInfo.START_COMPONENT_OTHER);
+ startInfo.setIntent(buildIntent(COMPONENT));
+
+ // Write the start info to a parcel.
+ Parcel parcel = Parcel.obtain();
+ startInfo.writeToParcel(parcel, 0 /* flags */);
+
+ // Set the data position back to 0 so it's ready to be read.
+ parcel.setDataPosition(0);
+
+ // Now load the record from the parcel.
+ ApplicationStartInfo startInfoFromParcel = new ApplicationStartInfo(parcel);
+
+ // Make sure there is no unread data remaining in the parcel, and confirm that the loaded
+ // start info object is equal to the one it was written from. Check dataAvail first as if
+ // that check fails then the next check will fail too, but knowing the status of this check
+ // will tell us that we're missing a read or write. Check the objects are equals second as
+ // if the avail check passes and equals fails, then we know we're reading all the data just
+ // not to the correct fields.
+ assertEquals(0, parcel.dataAvail());
+ assertTrue(startInfo.equals(startInfoFromParcel));
+ }
+
private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) {
try {
Field field = clazz.getDeclaredField(fieldName);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
index 24e7242..54ee2a3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java
@@ -33,6 +33,7 @@
import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.Process;
+import android.os.UserHandle;
import android.util.SparseArray;
import org.junit.After;
@@ -340,6 +341,24 @@
verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
}
+ @Test
+ public void registerPackageMonitor_callbackNotInAllowListSystemUidSecondUser_callbackIsCalled()
+ throws Exception {
+ IRemoteCallback callback = createMockPackageMonitorCallback();
+ int userId = 10;
+ int fakeAppId = 12345;
+ SparseArray<int[]> broadcastAllowList = new SparseArray<>();
+ broadcastAllowList.put(userId, new int[]{UserHandle.getUid(userId, fakeAppId)});
+
+ mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, userId,
+ UserHandle.getUid(userId, Process.SYSTEM_UID));
+ mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED,
+ FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{userId},
+ null /* instantUserIds */, broadcastAllowList, mHandler, null /* filterExtras */);
+
+ verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any());
+ }
+
private IRemoteCallback createMockPackageMonitorCallback() {
return spy(new IRemoteCallback.Stub() {
@Override
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index 0702926..a1db182 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -268,7 +268,7 @@
}
@Test
- public void testOnGlobalWakefulnessChangeStarted() throws Exception {
+ public void testOnGlobalWakefulnessChangeStarted() {
createNotifier();
// GIVEN system is currently non-interactive
when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
@@ -294,6 +294,96 @@
}
@Test
+ public void testOnGroupWakefulnessChangeStarted_newPowerGroup_perDisplayWakeDisabled() {
+ createNotifier();
+ // GIVEN power group is not yet known to Notifier and per-display wake by touch is disabled
+ final int groupId = 123;
+ final int changeReason = PowerManager.WAKE_REASON_TAP;
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+
+ // WHEN a power group wakefulness change starts
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN window manager policy is informed that device has started waking up
+ verify(mPolicy).startedWakingUp(groupId, changeReason);
+ verify(mDisplayManagerInternal, never()).getDisplayIds();
+ verify(mInputManagerInternal, never()).setDisplayInteractivities(any());
+ }
+
+ @Test
+ public void testOnGroupWakefulnessChangeStarted_interactivityNoChange_perDisplayWakeDisabled() {
+ createNotifier();
+ // GIVEN power group is not interactive and per-display wake by touch is disabled
+ final int groupId = 234;
+ final int changeReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT;
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_ASLEEP, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+ verify(mPolicy, times(1)).startedGoingToSleep(groupId, changeReason);
+
+ // WHEN a power wakefulness change to not interactive starts
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_ASLEEP, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN policy is only informed once of non-interactive wakefulness change
+ verify(mPolicy, times(1)).startedGoingToSleep(groupId, changeReason);
+ verify(mDisplayManagerInternal, never()).getDisplayIds();
+ verify(mInputManagerInternal, never()).setDisplayInteractivities(any());
+ }
+
+ @Test
+ public void testOnGroupWakefulnessChangeStarted_interactivityChange_perDisplayWakeDisabled() {
+ createNotifier();
+ // GIVEN power group is not interactive and per-display wake by touch is disabled
+ final int groupId = 345;
+ final int firstChangeReason = PowerManager.GO_TO_SLEEP_REASON_TIMEOUT;
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_ASLEEP, firstChangeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // WHEN a power wakefulness change to interactive starts
+ final int secondChangeReason = PowerManager.WAKE_REASON_TAP;
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, secondChangeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN policy is informed of the change
+ verify(mPolicy).startedWakingUp(groupId, secondChangeReason);
+ verify(mDisplayManagerInternal, never()).getDisplayIds();
+ verify(mInputManagerInternal, never()).setDisplayInteractivities(any());
+ }
+
+ @Test
+ public void testOnGroupWakefulnessChangeStarted_perDisplayWakeByTouchEnabled() {
+ createNotifier();
+ // GIVEN per-display wake by touch flag is enabled
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(true);
+ final int groupId = 456;
+ final int displayId1 = 1001;
+ final int displayId2 = 1002;
+ final int[] displays = new int[]{displayId1, displayId2};
+ when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displays));
+ when(mDisplayManagerInternal.getDisplayIdsForGroup(groupId)).thenReturn(displays);
+ final int changeReason = PowerManager.WAKE_REASON_TAP;
+
+ // WHEN power group wakefulness change started
+ mNotifier.onGroupWakefulnessChangeStarted(
+ groupId, WAKEFULNESS_AWAKE, changeReason, /* eventTime= */ 999);
+ mTestLooper.dispatchAll();
+
+ // THEN native input manager is updated that the displays are interactive
+ final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+ expectedDisplayInteractivities.put(displayId1, true);
+ expectedDisplayInteractivities.put(displayId2, true);
+ verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+ }
+
+ @Test
public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException {
when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
createNotifier();
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index cbe6700..6ede334 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -230,6 +230,20 @@
}
java_library {
+ name: "servicestests-utils-ravenwood",
+ srcs: [
+ "utils/**/*.java",
+ "utils/**/*.kt",
+ "utils-mockito/**/*.kt",
+ ],
+ libs: [
+ "android.test.runner.stubs.system",
+ "junit",
+ "mockito-ravenwood-prebuilt",
+ ],
+}
+
+java_library {
name: "mockito-test-utils",
srcs: [
"utils-mockito/**/*.kt",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index 1426d5d..c4b4afd 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -69,6 +69,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
+import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.LocalServices;
@@ -92,6 +93,8 @@
import org.testng.Assert;
import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
@RunWith(AndroidJUnit4.class)
public class FullScreenMagnificationControllerTest {
@@ -1440,19 +1443,42 @@
@Test
public void persistScale_setValueWhenScaleIsOne_nothingChanged() {
+ register(TEST_DISPLAY);
final float persistedScale =
mFullScreenMagnificationController.getPersistedScale(TEST_DISPLAY);
PointF pivotPoint = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
- mFullScreenMagnificationController.setScale(DISPLAY_0, 1.0f, pivotPoint.x, pivotPoint.y,
+ mFullScreenMagnificationController.setScale(TEST_DISPLAY, 1.0f, pivotPoint.x, pivotPoint.y,
false, SERVICE_ID_1);
mFullScreenMagnificationController.persistScale(TEST_DISPLAY);
+ // persistScale may post a task to a background thread. Let's wait for it completes.
+ waitForBackgroundThread();
Assert.assertEquals(mFullScreenMagnificationController.getPersistedScale(TEST_DISPLAY),
persistedScale);
}
@Test
+ public void persistScale_setValuesOnMultipleDisplays() {
+ register(DISPLAY_0);
+ register(DISPLAY_1);
+ final PointF pivotPoint = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+ mFullScreenMagnificationController.setScale(DISPLAY_0, 3.0f, pivotPoint.x, pivotPoint.y,
+ false, SERVICE_ID_1);
+ mFullScreenMagnificationController.persistScale(DISPLAY_0);
+ mFullScreenMagnificationController.setScale(DISPLAY_1, 4.0f, pivotPoint.x, pivotPoint.y,
+ false, SERVICE_ID_1);
+ mFullScreenMagnificationController.persistScale(DISPLAY_1);
+
+ // persistScale may post a task to a background thread. Let's wait for it completes.
+ waitForBackgroundThread();
+ Assert.assertEquals(mFullScreenMagnificationController.getPersistedScale(DISPLAY_0),
+ 3.0f);
+ Assert.assertEquals(mFullScreenMagnificationController.getPersistedScale(DISPLAY_1),
+ 4.0f);
+ }
+
+ @Test
public void testOnContextChanged_alwaysOnFeatureDisabled_resetMagnification() {
setScaleToMagnifying();
@@ -1494,6 +1520,15 @@
);
}
+ private static void waitForBackgroundThread() {
+ final CompletableFuture<Void> future = new CompletableFuture<>();
+ BackgroundThread.getHandler().post(() -> future.complete(null));
+ try {
+ future.get();
+ } catch (InterruptedException | ExecutionException ignore) {
+ }
+ }
+
private void setScaleToMagnifying() {
register(DISPLAY_0);
float scale = 2.0f;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
index 87fe6cf..6aa8a32 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
@@ -300,7 +300,8 @@
mMagnificationConnectionManager.setConnection(mMockConnection.getConnection());
mMagnificationConnectionManager.enableWindowMagnification(TEST_DISPLAY, 2.5f, NaN, NaN);
- mMagnificationConnectionManager.setScale(TEST_DISPLAY, 10.0f);
+ mMagnificationConnectionManager.setScale(TEST_DISPLAY,
+ MagnificationScaleProvider.MAX_SCALE * 2.f);
assertEquals(mMagnificationConnectionManager.getScale(TEST_DISPLAY),
MagnificationScaleProvider.MAX_SCALE);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
index efc2d97..1bea371 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/UtilsTest.java
@@ -217,7 +217,13 @@
doNothing().when(mContext).enforceCallingOrSelfPermission(
eq(SET_BIOMETRIC_DIALOG_ADVANCED), any());
- assertTrue(Utils.isValidAuthenticatorConfig(mContext, Authenticators.MANDATORY_BIOMETRICS));
+ if (Flags.mandatoryBiometrics()) {
+ assertTrue(Utils.isValidAuthenticatorConfig(mContext,
+ Authenticators.MANDATORY_BIOMETRICS));
+ } else {
+ assertFalse(Utils.isValidAuthenticatorConfig(mContext,
+ Authenticators.MANDATORY_BIOMETRICS));
+ }
// The rest of the bits are not allowed to integrate with the public APIs
for (int i = 8; i < 32; i++) {
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index ee63d5d..425bb15 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -33,6 +33,7 @@
import static android.view.Display.INVALID_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -51,11 +52,15 @@
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
+import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions.LaunchCookie;
import android.app.AppOpsManager;
+import android.app.Instrumentation;
import android.app.KeyguardManager;
+import android.app.role.RoleManager;
+import android.companion.AssociationRequest;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -68,6 +73,7 @@
import android.os.Binder;
import android.os.IBinder;
import android.os.Looper;
+import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.test.TestLooper;
@@ -88,6 +94,7 @@
import com.android.server.wm.WindowManagerInternal;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -98,6 +105,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -312,7 +320,6 @@
assertThat(mService.getActiveProjectionInfo()).isNotNull();
}
- @SuppressLint("MissingPermission")
@EnableFlags(android.companion.virtualdevice.flags
.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
@Test
@@ -335,6 +342,36 @@
assertThat(mService.getActiveProjectionInfo()).isNotNull();
}
+ @EnableFlags(android.companion.virtualdevice.flags
+ .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ @Test
+ public void testCreateProjection_keyguardLocked_RoleHeld() {
+ runWithRole(AssociationRequest.DEVICE_PROFILE_APP_STREAMING, () -> {
+ try {
+ mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
+ doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
+ any(ApplicationInfoFlags.class), any(UserHandle.class));
+ MediaProjectionManagerService.MediaProjection projection =
+ mService.createProjectionInternal(Process.myUid(),
+ mContext.getPackageName(),
+ TYPE_MIRRORING, /* isPermanentGrant= */ false, UserHandle.CURRENT);
+ doReturn(true).when(mKeyguardManager).isKeyguardLocked();
+ doReturn(PackageManager.PERMISSION_DENIED).when(
+ mPackageManager).checkPermission(
+ RECORD_SENSITIVE_CONTENT, projection.packageName);
+
+ projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
+
+ // The projection was started because it was allowed to capture the keyguard.
+ assertWithMessage("Failed to run projection")
+ .that(mService.getActiveProjectionInfo()).isNotNull();
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
@Test
public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
throws NameNotFoundException {
@@ -1202,6 +1239,47 @@
return mService.getProjectionInternal(UID, PACKAGE_NAME);
}
+ /**
+ * Run the provided block giving the current context's package the provided role.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private void runWithRole(String role, Runnable block) {
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ String packageName = mContext.getPackageName();
+ UserHandle user = instrumentation.getTargetContext().getUser();
+ RoleManager roleManager = Objects.requireNonNull(
+ mContext.getSystemService(RoleManager.class));
+ try {
+ CountDownLatch latch = new CountDownLatch(1);
+ instrumentation.getUiAutomation().adoptShellPermissionIdentity(
+ Manifest.permission.MANAGE_ROLE_HOLDERS,
+ Manifest.permission.BYPASS_ROLE_QUALIFICATION);
+
+ roleManager.setBypassingRoleQualification(true);
+ roleManager.addRoleHolderAsUser(role, packageName, /* flags = */ 0, user,
+ mContext.getMainExecutor(), success -> {
+ if (success) {
+ latch.countDown();
+ } else {
+ Assert.fail("Couldn't set role for test (failure) " + role);
+ }
+ });
+ assertWithMessage("Couldn't set role for test (timeout) : " + role)
+ .that(latch.await(1, TimeUnit.SECONDS)).isTrue();
+ block.run();
+
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ roleManager.removeRoleHolderAsUser(role, packageName, 0, user,
+ mContext.getMainExecutor(), (aBool) -> {
+ });
+ roleManager.setBypassingRoleQualification(false);
+ instrumentation.getUiAutomation()
+ .dropShellPermissionIdentity();
+ }
+ }
+
private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub {
CountDownLatch mLatch = new CountDownLatch(1);
@Override
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 585df84..22a4f85 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2662,6 +2662,17 @@
when(n.isColorized()).thenReturn(true);
when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
assertThat(GroupHelper.getSection(notification_colorFg)).isNull();
+
+ NotificationRecord notification_media = spy(getNotificationRecord(mPkg, 0, "", mUser,
+ "", false, IMPORTANCE_LOW));
+ n = mock(Notification.class);
+ sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+ when(notification_media.isConversation()).thenReturn(false);
+ when(notification_media.getNotification()).thenReturn(n);
+ when(notification_media.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isMediaNotification()).thenReturn(true);
+ assertThat(GroupHelper.getSection(notification_media)).isNull();
}
@Test
@@ -2756,7 +2767,7 @@
@Test
@EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS})
public void testNonGroupableNotifications_forceGroupConversations() {
- // Check that there is no valid section for: calls, foreground services
+ // Check that there is no valid section for: calls, foreground services, media notifications
NotificationRecord notification_call = spy(getNotificationRecord(mPkg, 0, "", mUser,
"", false, IMPORTANCE_LOW));
Notification n = mock(Notification.class);
@@ -2780,6 +2791,17 @@
when(n.isColorized()).thenReturn(true);
when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
assertThat(GroupHelper.getSection(notification_colorFg)).isNull();
+
+ NotificationRecord notification_media = spy(getNotificationRecord(mPkg, 0, "", mUser,
+ "", false, IMPORTANCE_LOW));
+ n = mock(Notification.class);
+ sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+ when(notification_media.isConversation()).thenReturn(false);
+ when(notification_media.getNotification()).thenReturn(n);
+ when(notification_media.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isMediaNotification()).thenReturn(true);
+ assertThat(GroupHelper.getSection(notification_media)).isNull();
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index a45b102..797b95b5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -15,12 +15,15 @@
*/
package com.android.server.notification;
+import static android.os.UserHandle.USER_ALL;
+
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
-import static junit.framework.Assert.fail;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
@@ -33,6 +36,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.Manifest;
import android.app.ActivityManager;
import android.app.INotificationManager;
import android.content.ComponentName;
@@ -42,24 +46,28 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
-import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.service.notification.Adjustment;
import android.testing.TestableContext;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.IntArray;
import android.util.Xml;
-import android.Manifest;
+
+import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.CollectionUtils;
-import com.android.internal.util.function.TriPredicate;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.UiServiceTestCase;
import com.android.server.notification.NotificationManagerService.NotificationAssistants;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -71,8 +79,12 @@
import java.util.Arrays;
import java.util.List;
+@RunWith(AndroidJUnit4.class)
public class NotificationAssistantsTest extends UiServiceTestCase {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private PackageManager mPm;
@Mock
@@ -98,6 +110,35 @@
ComponentName mCn = new ComponentName("a", "b");
+
+ // Helper function to hold mApproved lock, avoid GuardedBy lint errors
+ private boolean isUserSetServicesEmpty(NotificationAssistants assistant, int userId) {
+ synchronized (assistant.mApproved) {
+ return assistant.mUserSetServices.get(userId).isEmpty();
+ }
+ }
+
+ private void writeXmlAndReload(int userId) throws Exception {
+ TypedXmlSerializer serializer = Xml.newFastSerializer();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+ serializer.startDocument(null, true);
+ mAssistants.writeXml(serializer, false, userId);
+ serializer.endDocument();
+ serializer.flush();
+
+ //fail(baos.toString("UTF-8"));
+
+ final TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), null);
+
+ parser.nextTag();
+ mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm));
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
+ }
+
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -164,25 +205,7 @@
mAssistants.setPackageOrComponentEnabled(current.flattenToString(), userId, true, false,
true);
- TypedXmlSerializer serializer = Xml.newFastSerializer();
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
- serializer.startDocument(null, true);
- mAssistants.writeXml(serializer, true, userId);
- serializer.endDocument();
- serializer.flush();
-
- //fail(baos.toString("UTF-8"));
-
- final TypedXmlPullParser parser = Xml.newFastPullParser();
- parser.setInput(new BufferedInputStream(
- new ByteArrayInputStream(baos.toByteArray())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
-
- parser.nextTag();
- mAssistants = spy(mNm.new NotificationAssistants(mContext, mLock, mUserProfiles, miPm));
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ writeXmlAndReload(USER_ALL);
ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
// approved should not be null
@@ -203,11 +226,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
@@ -226,11 +247,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, true,
+ mAssistants.readXml(parser, mNm::canUseManagedServices, true,
ActivityManager.getCurrentUser());
ArrayMap<Boolean, ArraySet<String>> approved = mAssistants.mApproved.get(0);
@@ -253,11 +272,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(1)).upgradeUserSet();
assertTrue(mAssistants.mIsUserChanged.get(0));
@@ -273,11 +290,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(0)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -294,11 +309,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(0)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -314,11 +327,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(1)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -334,11 +345,9 @@
final TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
- TriPredicate<String, Integer, String> allowedManagedServicePackages =
- mNm::canUseManagedServices;
parser.nextTag();
- mAssistants.readXml(parser, allowedManagedServicePackages, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, mNm::canUseManagedServices, false, USER_ALL);
verify(mAssistants, times(1)).upgradeUserSet();
assertTrue(isUserSetServicesEmpty(mAssistants, 0));
@@ -361,7 +370,7 @@
new ByteArrayInputStream(xml.toString().getBytes())), null);
parser.nextTag();
- mAssistants.readXml(parser, null, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, null, false, USER_ALL);
assertEquals(1, mAssistants.getAllowedComponents(0).size());
assertEquals(new ArrayList(Arrays.asList(new ComponentName("a", "a"))),
@@ -378,7 +387,7 @@
parser.setInput(new BufferedInputStream(
new ByteArrayInputStream(xml.toString().getBytes())), null);
parser.nextTag();
- mAssistants.readXml(parser, null, false, UserHandle.USER_ALL);
+ mAssistants.readXml(parser, null, false, USER_ALL);
verify(mNm, never()).setDefaultAssistantForUser(anyInt());
verify(mAssistants, times(1)).addApprovedList(
@@ -529,10 +538,66 @@
assertEquals(new ArraySet<>(), mAssistants.getDefaultComponents());
}
- // Helper function to hold mApproved lock, avoid GuardedBy lint errors
- private boolean isUserSetServicesEmpty(NotificationAssistants assistant, int userId) {
- synchronized (assistant.mApproved) {
- return assistant.mUserSetServices.get(userId).isEmpty();
- }
+ @Test
+ @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void testSetAdjustmentTypeSupportedState() throws Exception {
+ int userId = ActivityManager.getCurrentUser();
+
+ mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+ true, true);
+ ComponentName current = CollectionUtils.firstOrNull(
+ mAssistants.getAllowedComponents(userId));
+ assertNotNull(current);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(0);
+
+ ManagedServices.ManagedServiceInfo info =
+ mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
+ mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId)).contains(
+ Adjustment.KEY_NOT_CONVERSATION);
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(1);
}
-}
+
+ @Test
+ @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void testSetAdjustmentTypeSupportedState_readWriteXml_entries() throws Exception {
+ int userId = ActivityManager.getCurrentUser();
+
+ mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+ true, true);
+ ComponentName current = CollectionUtils.firstOrNull(
+ mAssistants.getAllowedComponents(userId));
+ assertNotNull(current);
+
+ ManagedServices.ManagedServiceInfo info =
+ mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
+ mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false);
+
+ writeXmlAndReload(USER_ALL);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId)).contains(
+ Adjustment.KEY_NOT_CONVERSATION);
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(1);
+ }
+
+ @Test
+ @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public void testSetAdjustmentTypeSupportedState_readWriteXml_empty() throws Exception {
+ int userId = ActivityManager.getCurrentUser();
+
+ mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
+ true, true);
+ ComponentName current = CollectionUtils.firstOrNull(
+ mAssistants.getAllowedComponents(userId));
+ assertNotNull(current);
+
+ writeXmlAndReload(USER_ALL);
+
+ assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(0);
+ }
+}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index bbf2cbd..3c120e1 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -33,6 +33,7 @@
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.Notification.FLAG_CAN_COLORIZE;
import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
+import static android.app.Notification.FLAG_GROUP_SUMMARY;
import static android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
import static android.app.Notification.FLAG_NO_CLEAR;
import static android.app.Notification.FLAG_NO_DISMISS;
@@ -2872,6 +2873,131 @@
}
@Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testEnqueueNotification_forceGrouped_clearsSummaryFlag() throws Exception {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ // Old record was a summary and it was auto-grouped
+ final NotificationRecord r =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true);
+ mService.addNotification(r);
+ mService.convertSummaryToNotificationLocked(r.getKey());
+ mService.addAutogroupKeyLocked(r.getKey(), aggregateGroupName, true);
+
+ assertThat(mService.mNotificationList).hasSize(1);
+
+ // Update record is a summary
+ final Notification updatedNotification = generateNotificationRecord(
+ mTestNotificationChannel, 0, originalGroupName, true).getNotification();
+ assertThat(updatedNotification.flags & FLAG_GROUP_SUMMARY).isEqualTo(FLAG_GROUP_SUMMARY);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(),
+ r.getSbn().getId(), updatedNotification, r.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that FLAG_GROUP_SUMMARY was removed
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testEnqueueNotification_forceGroupedRegular_updatedAsSummary_clearsSummaryFlag()
+ throws Exception {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ // Old record was not summary and it was auto-grouped
+ final NotificationRecord r =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+ mService.addNotification(r);
+ mService.addAutogroupKeyLocked(r.getKey(), aggregateGroupName, true);
+ assertThat(mService.mNotificationList).hasSize(1);
+
+ // Update record is a summary
+ final Notification updatedNotification = generateNotificationRecord(
+ mTestNotificationChannel, 0, originalGroupName, true).getNotification();
+ assertThat(updatedNotification.flags & FLAG_GROUP_SUMMARY).isEqualTo(FLAG_GROUP_SUMMARY);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(),
+ r.getSbn().getId(), updatedNotification, r.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that FLAG_GROUP_SUMMARY was removed
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testEnqueueNotification_notForceGrouped_dontClearSummaryFlag()
+ throws Exception {
+ final String originalGroupName = "originalGroup";
+
+ // Old record was a summary and it was not auto-grouped
+ final NotificationRecord r =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, true);
+ mService.addNotification(r);
+ assertThat(mService.mNotificationList).hasSize(1);
+
+ // Update record is a summary
+ final Notification updatedNotification = generateNotificationRecord(
+ mTestNotificationChannel, 0, originalGroupName, true).getNotification();
+ assertThat(updatedNotification.flags & FLAG_GROUP_SUMMARY).isEqualTo(FLAG_GROUP_SUMMARY);
+
+ mBinderService.enqueueNotificationWithTag(mPkg, mPkg, r.getSbn().getTag(),
+ r.getSbn().getId(), updatedNotification, r.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that FLAG_GROUP_SUMMARY was not removed
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(
+ FLAG_GROUP_SUMMARY);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testRemoveFGSFlagFromNotification_enqueued_forceGrouped_clearsSummaryFlag() {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null,
+ originalGroupName, true);
+ r.getSbn().getNotification().flags &= ~FLAG_GROUP_SUMMARY;
+ r.setOverrideGroupKey(aggregateGroupName);
+ mService.addEnqueuedNotification(r);
+
+ mInternalService.removeForegroundServiceFlagFromNotification(
+ mPkg, r.getSbn().getId(), r.getSbn().getUserId());
+ waitForIdle();
+
+ assertThat(mService.mEnqueuedNotifications).hasSize(1);
+ assertThat(mService.mEnqueuedNotifications.get(0).getFlags() & FLAG_GROUP_SUMMARY)
+ .isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testRemoveFGSFlagFromNotification_posted_forceGrouped_clearsSummaryFlag() {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+
+ final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, null,
+ originalGroupName, true);
+ r.getSbn().getNotification().flags &= ~FLAG_GROUP_SUMMARY;
+ r.setOverrideGroupKey(aggregateGroupName);
+ mService.addNotification(r);
+
+ mInternalService.removeForegroundServiceFlagFromNotification(
+ mPkg, r.getSbn().getId(), r.getSbn().getUserId());
+ waitForIdle();
+
+ assertThat(mService.mNotificationList).hasSize(1);
+ assertThat(mService.mNotificationList.get(0).getFlags() & FLAG_GROUP_SUMMARY).isEqualTo(0);
+ }
+
+ @Test
public void testCancelAllNotifications_IgnoreForegroundService() throws Exception {
when(mAmi.applyForegroundServiceNotification(
any(), anyString(), anyInt(), anyString(), anyInt())).thenReturn(SHOW_IMMEDIATELY);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 91eb2ed..c6cc941 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -60,6 +60,7 @@
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
@@ -69,6 +70,7 @@
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper
@@ -127,7 +129,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.ZenRule.class, getZenRuleExemptFields());
+ ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false);
generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
@@ -145,6 +147,337 @@
}
}
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void testRuleDiff_toStringNoChangeAddRemove() throws Exception {
+ // Start with two identical rules
+ ZenModeConfig.ZenRule r1 = makeRule();
+ ZenModeConfig.ZenRule r2 = makeRule();
+
+ ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{no changes}");
+
+ d = new ZenModeDiff.RuleDiff(r1, null);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{removed}");
+
+ d = new ZenModeDiff.RuleDiff(null, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{added}");
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void testRuleDiff_toString() throws Exception {
+ // Start with two identical rules
+ ZenModeConfig.ZenRule r1 = makeRule();
+ ZenModeConfig.ZenRule r2 = makeRule();
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false);
+ generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
+
+ ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{"
+ + "enabled:true->false, "
+ + "conditionOverride:2->1, "
+ + "name:string1->string2, "
+ + "zenMode:2->1, "
+ + "conditionId:null->, "
+ + "condition:null->Condition["
+ + "state=STATE_TRUE,"
+ + "id=hello:,"
+ + "summary=,"
+ + "line1=,"
+ + "line2=,"
+ + "icon=-1,"
+ + "source=SOURCE_UNKNOWN,"
+ + "flags=2], "
+ + "component:null->ComponentInfo{b/b}, "
+ + "configurationActivity:null->ComponentInfo{a/a}, "
+ + "id:string1->string2, "
+ + "creationTime:200->100, "
+ + "enabler:string1->string2, "
+ + "zenPolicy:ZenPolicyDiff{"
+ + "mPriorityCategories_Reminders:1->2, "
+ + "mPriorityCategories_Events:1->2, "
+ + "mPriorityCategories_Messages:1->2, "
+ + "mPriorityCategories_Calls:1->2, "
+ + "mPriorityCategories_RepeatCallers:1->2, "
+ + "mPriorityCategories_Alarms:1->2, "
+ + "mPriorityCategories_Media:1->2, "
+ + "mPriorityCategories_System:1->2, "
+ + "mPriorityCategories_Conversations:1->2, "
+ + "mVisualEffects_FullScreenIntent:1->2, "
+ + "mVisualEffects_Lights:1->2, "
+ + "mVisualEffects_Peek:1->2, "
+ + "mVisualEffects_StatusBar:1->2, "
+ + "mVisualEffects_Badge:1->2, "
+ + "mVisualEffects_Ambient:1->2, "
+ + "mVisualEffects_NotificationList:1->2, "
+ + "mPriorityMessages:2->1, "
+ + "mPriorityCalls:2->1, "
+ + "mConversationSenders:2->1, "
+ + "mAllowChannels:2->1}, "
+ + "modified:true->false, "
+ + "pkg:string1->string2, "
+ + "zenDeviceEffects:ZenDeviceEffectsDiff{"
+ + "mGrayscale:true->false, "
+ + "mSuppressAmbientDisplay:true->false, "
+ + "mDimWallpaper:true->false, "
+ + "mNightMode:true->false, "
+ + "mDisableAutoBrightness:true->false, "
+ + "mDisableTapToWake:true->false, "
+ + "mDisableTiltToWake:true->false, "
+ + "mDisableTouch:true->false, "
+ + "mMinimizeRadioUsage:true->false, "
+ + "mMaximizeDoze:true->false, "
+ + "mExtraEffects:[effect1]->[effect2]}, "
+ + "triggerDescription:string1->string2, "
+ + "type:2->1, "
+ + "allowManualInvocation:true->false, "
+ + "iconResName:string1->string2, "
+ + "legacySuppressedEffects:2->1}");
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void testRuleDiff_toStringNullStartPolicy() throws Exception {
+ // Start with two identical rules
+ ZenModeConfig.ZenRule r1 = makeRule();
+ ZenModeConfig.ZenRule r2 = makeRule();
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenModeConfig.ZenRule.class, getZenRuleExemptFields(), false);
+ generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
+
+ // Create a ZenRule with ZenDeviceEffects and ZenPolicy as null.
+ r1.zenPolicy = null;
+ r1.zenDeviceEffects = null;
+ ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
+ assertThat(d.toString()).isEqualTo("ZenRuleDiff{"
+ + "enabled:true->false, "
+ + "conditionOverride:2->1, "
+ + "name:string1->string2, "
+ + "zenMode:2->1, "
+ + "conditionId:null->, "
+ + "condition:null->Condition["
+ + "state=STATE_TRUE,"
+ + "id=hello:,"
+ + "summary=,"
+ + "line1=,"
+ + "line2=,"
+ + "icon=-1,"
+ + "source=SOURCE_UNKNOWN,"
+ + "flags=2], "
+ + "component:null->ComponentInfo{b/b}, "
+ + "configurationActivity:null->ComponentInfo{a/a}, "
+ + "id:string1->string2, "
+ + "creationTime:200->100, "
+ + "enabler:string1->string2, "
+ + "zenPolicy:ZenPolicyDiff{added}, "
+ + "modified:true->false, "
+ + "pkg:string1->string2, "
+ + "zenDeviceEffects:ZenDeviceEffectsDiff{added}, "
+ + "triggerDescription:string1->string2, "
+ + "type:2->1, "
+ + "allowManualInvocation:true->false, "
+ + "iconResName:string1->string2, "
+ + "legacySuppressedEffects:2->1}");
+ }
+
+ @Test
+ public void testDeviceEffectsDiff_addRemoveSame() {
+ // Test add, remove, and both sides same
+ ZenDeviceEffects effects = new ZenDeviceEffects.Builder().build();
+
+ // Both sides same rule
+ ZenModeDiff.DeviceEffectsDiff dSame = new ZenModeDiff.DeviceEffectsDiff(effects, effects);
+ assertFalse(dSame.hasDiff());
+
+ // from existent rule to null: expect deleted
+ ZenModeDiff.DeviceEffectsDiff deleted = new ZenModeDiff.DeviceEffectsDiff(effects, null);
+ assertTrue(deleted.hasDiff());
+ assertTrue(deleted.wasRemoved());
+
+ // from null to new rule: expect added
+ ZenModeDiff.DeviceEffectsDiff added = new ZenModeDiff.DeviceEffectsDiff(null, effects);
+ assertTrue(added.hasDiff());
+ assertTrue(added.wasAdded());
+ }
+
+ @Test
+ public void testDeviceEffectsDiff_fieldDiffs() throws Exception {
+ // Start these the same
+ ZenDeviceEffects effects1 = new ZenDeviceEffects.Builder().build();
+ ZenDeviceEffects effects2 = new ZenDeviceEffects.Builder().build();
+
+ // maps mapping field name -> expected output value as we set diffs
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenDeviceEffects.class, Collections.emptySet() /*no exempt fields*/, true);
+ generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo);
+
+ ZenModeDiff.DeviceEffectsDiff d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
+ assertTrue(d.hasDiff());
+
+ // Now diff them and check that each of the fields has a diff
+ for (Field f : fieldsForDiff) {
+ String name = f.getName();
+ assertNotNull("diff not found for field: " + name, d.getDiffForField(name));
+ assertTrue(d.getDiffForField(name).hasDiff());
+ assertTrue("unexpected field: " + name, expectedFrom.containsKey(name));
+ assertTrue("unexpected field: " + name, expectedTo.containsKey(name));
+ assertEquals(expectedFrom.get(name), d.getDiffForField(name).from());
+ assertEquals(expectedTo.get(name), d.getDiffForField(name).to());
+ }
+ }
+
+ @Test
+ public void testDeviceEffectsDiff_toString() throws Exception {
+ // Ensure device effects toString is readable.
+ ZenDeviceEffects effects1 = new ZenDeviceEffects.Builder().build();
+ ZenDeviceEffects effects2 = new ZenDeviceEffects.Builder().build();
+
+ ZenModeDiff.DeviceEffectsDiff d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{no changes}");
+
+ d = new ZenModeDiff.DeviceEffectsDiff(effects1, null);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{removed}");
+
+ d = new ZenModeDiff.DeviceEffectsDiff(null, effects2);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{added}");
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenDeviceEffects.class, Collections.emptySet() /*no exempt fields*/, true);
+ generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo);
+
+ d = new ZenModeDiff.DeviceEffectsDiff(effects1, effects2);
+ assertThat(d.toString()).isEqualTo("ZenDeviceEffectsDiff{"
+ + "mGrayscale:true->false, "
+ + "mSuppressAmbientDisplay:true->false, "
+ + "mDimWallpaper:true->false, "
+ + "mNightMode:true->false, "
+ + "mDisableAutoBrightness:true->false, "
+ + "mDisableTapToWake:true->false, "
+ + "mDisableTiltToWake:true->false, "
+ + "mDisableTouch:true->false, "
+ + "mMinimizeRadioUsage:true->false, "
+ + "mMaximizeDoze:true->false, "
+ + "mExtraEffects:[effect1]->[effect2]}");
+ }
+
+
+ @Test
+ public void testPolicyDiff_addRemoveSame() {
+ // Test add, remove, and both sides same
+ ZenPolicy effects = new ZenPolicy.Builder().build();
+
+ // Both sides same rule
+ ZenModeDiff.PolicyDiff dSame = new ZenModeDiff.PolicyDiff(effects, effects);
+ assertFalse(dSame.hasDiff());
+
+ // from existent rule to null: expect deleted
+ ZenModeDiff.PolicyDiff deleted = new ZenModeDiff.PolicyDiff(effects, null);
+ assertTrue(deleted.hasDiff());
+ assertTrue(deleted.wasRemoved());
+
+ // from null to new rule: expect added
+ ZenModeDiff.PolicyDiff added = new ZenModeDiff.PolicyDiff(null, effects);
+ assertTrue(added.hasDiff());
+ assertTrue(added.wasAdded());
+ }
+
+ @Test
+ public void testPolicyDiff_fieldDiffs() throws Exception {
+ // Start these the same
+ ZenPolicy policy1 = new ZenPolicy.Builder().build();
+ ZenPolicy policy2 = new ZenPolicy.Builder().build();
+
+ // maps mapping field name -> expected output value as we set diffs
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(ZenPolicy.class, Collections.emptySet(),
+ false);
+ generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom, expectedTo);
+
+ ZenModeDiff.PolicyDiff d = new ZenModeDiff.PolicyDiff(policy1, policy2);
+ assertTrue(d.hasDiff());
+
+ // Now diff them and check that each of the fields has a diff.
+ // Because ZenPolicy consolidates priority category and visual effect fields in a list,
+ // we cannot use reflection on ZenPolicy to get the list of fields.
+ ArrayList<String> diffFields = new ArrayList<>();
+ Field[] fields = ZenModeDiff.PolicyDiff.class.getDeclaredFields();
+
+ for (Field field : fields) {
+ int m = field.getModifiers();
+ if (Modifier.isStatic(m) && Modifier.isFinal(m)) {
+ diffFields.add((String) field.get(policy1));
+ }
+ }
+
+ for (String name : diffFields) {
+ assertNotNull("diff not found for field: " + name, d.getDiffForField(name));
+ assertTrue(d.getDiffForField(name).hasDiff());
+ assertTrue("unexpected field: " + name, expectedFrom.containsKey(name));
+ assertTrue("unexpected field: " + name, expectedTo.containsKey(name));
+ assertEquals(expectedFrom.get(name), d.getDiffForField(name).from());
+ assertEquals(expectedTo.get(name), d.getDiffForField(name).to());
+ }
+ }
+
+ @Test
+ public void testPolicyDiff_toString() throws Exception {
+ // Ensure device effects toString is readable.
+ ZenPolicy policy1 = new ZenPolicy.Builder().build();
+ ZenPolicy policy2 = new ZenPolicy.Builder().build();
+
+ ZenModeDiff.PolicyDiff d = new ZenModeDiff.PolicyDiff(policy1, policy2);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{no changes}");
+
+ d = new ZenModeDiff.PolicyDiff(policy1, null);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{removed}");
+
+ d = new ZenModeDiff.PolicyDiff(null, policy2);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{added}");
+
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenPolicy.class, Collections.emptySet() /*no exempt fields*/, false);
+ generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom, expectedTo);
+
+ d = new ZenModeDiff.PolicyDiff(policy1, policy2);
+ assertThat(d.toString()).isEqualTo("ZenPolicyDiff{"
+ + "mPriorityCategories_Reminders:1->2, "
+ + "mPriorityCategories_Events:1->2, "
+ + "mPriorityCategories_Messages:1->2, "
+ + "mPriorityCategories_Calls:1->2, "
+ + "mPriorityCategories_RepeatCallers:1->2, "
+ + "mPriorityCategories_Alarms:1->2, "
+ + "mPriorityCategories_Media:1->2, "
+ + "mPriorityCategories_System:1->2, "
+ + "mPriorityCategories_Conversations:1->2, "
+ + "mVisualEffects_FullScreenIntent:1->2, "
+ + "mVisualEffects_Lights:1->2, "
+ + "mVisualEffects_Peek:1->2, "
+ + "mVisualEffects_StatusBar:1->2, "
+ + "mVisualEffects_Badge:1->2, "
+ + "mVisualEffects_Ambient:1->2, "
+ + "mVisualEffects_NotificationList:1->2, "
+ + "mPriorityMessages:2->1, "
+ + "mPriorityCalls:2->1, "
+ + "mConversationSenders:2->1, "
+ + "mAllowChannels:2->1}");
+ }
+
private static Set<String> getZenRuleExemptFields() {
// "Metadata" fields are never compared.
Set<String> exemptFields = new LinkedHashSet<>(
@@ -194,7 +527,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.class, getConfigExemptAndFlaggedFields());
+ ZenModeConfig.class, getConfigExemptAndFlaggedFields(), false);
generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2);
@@ -223,7 +556,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS);
+ ZenModeConfig.class, ZEN_MODE_CONFIG_EXEMPT_FIELDS, false);
generateFieldDiffs(c1, c2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.ConfigDiff d = new ZenModeDiff.ConfigDiff(c1, c2);
@@ -359,17 +692,23 @@
// Get the fields on which we would want to check a diff. The requirements are: not final or/
// static (as these should/can never change), and not in a specific list that's exempted.
- private List<Field> getFieldsForDiffCheck(Class<?> c, Set<String> exemptNames)
+ private List<Field> getFieldsForDiffCheck(Class<?> c, Set<String> exemptNames,
+ boolean includeFinal)
throws SecurityException {
Field[] fields = c.getDeclaredFields();
ArrayList<Field> out = new ArrayList<>();
for (Field field : fields) {
// Check for exempt reasons
+ // Anything in provided exemptNames is skipped.
+ if (exemptNames.contains(field.getName())) {
+ continue;
+ }
int m = field.getModifiers();
- if (Modifier.isFinal(m)
- || Modifier.isStatic(m)
- || exemptNames.contains(field.getName())) {
+ if (Modifier.isStatic(m)) {
+ continue;
+ }
+ if (!includeFinal && Modifier.isFinal(m)) {
continue;
}
out.add(field);
@@ -377,6 +716,106 @@
return out;
}
+ // Generate a set of diffs for two ZenPolicy objects. Store the results in the provided
+ // expectation maps.
+ private void generateFieldDiffsForZenPolicy(ZenPolicy a, ZenPolicy b, List<Field> fields,
+ ArrayMap<String, Object> expectedA, ArrayMap<String, Object> expectedB)
+ throws Exception {
+ // Loop through fields for which we want to check diffs, set a diff and keep track of
+ // what we set.
+ for (Field f : fields) {
+ f.setAccessible(true);
+ // Just double-check also that the fields actually are for the class declared
+ assertEquals(f.getDeclaringClass(), a.getClass());
+ Class<?> t = f.getType();
+
+ if (int.class.equals(t)) {
+ // these will not be valid for arbitrary int enums, but should suffice for a diff.
+ f.setInt(a, 2);
+ expectedA.put(f.getName(), 2);
+ f.setInt(b, 1);
+ expectedB.put(f.getName(), 1);
+ } else if (List.class.equals(t)) {
+ // Fieds mPriorityCategories and mVisualEffects store multiple values and
+ // must be treated separately.
+ List<Integer> aList = (ArrayList<Integer>) f.get(a);
+ List<Integer> bList = (ArrayList<Integer>) f.get(b);
+ if (f.getName().equals("mPriorityCategories")) {
+ // PRIORITY_CATEGORY_REMINDERS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 0,
+ "mPriorityCategories_Reminders");
+ // PRIORITY_CATEGORY_EVENTS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 1,
+ "mPriorityCategories_Events");
+ // PRIORITY_CATEGORY_MESSAGES
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 2,
+ "mPriorityCategories_Messages");
+ // PRIORITY_CATEGORY_CALLS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 3,
+ "mPriorityCategories_Calls");
+ // PRIORITY_CATEGORY_REPEAT_CALLERS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 4,
+ "mPriorityCategories_RepeatCallers");
+ // PRIORITY_CATEGORY_ALARMS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 5,
+ "mPriorityCategories_Alarms");
+ // PRIORITY_CATEGORY_MEDIA
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 6,
+ "mPriorityCategories_Media");
+ // PRIORITY_CATEGORY_SYSTEM
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 7,
+ "mPriorityCategories_System");
+ // PRIORITY_CATEGORY_CONVERSATIONS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 8,
+ "mPriorityCategories_Conversations");
+ // Assert that we've set every PriorityCategory enum value.
+ assertThat(Collections.frequency(aList, ZenPolicy.STATE_ALLOW))
+ .isEqualTo(ZenPolicy.NUM_PRIORITY_CATEGORIES);
+ } else if (f.getName().equals("mVisualEffects")) {
+ // VISUAL_EFFECT_FULL_SCREEN_INTENT
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 0,
+ "mVisualEffects_FullScreenIntent");
+ // VISUAL_EFFECT_LIGHTS
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 1,
+ "mVisualEffects_Lights");
+ // VISUAL_EFFECT_PEEK
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 2,
+ "mVisualEffects_Peek");
+ // VISUAL_EFFECT_STATUS_BAR
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 3,
+ "mVisualEffects_StatusBar");
+ // VISUAL_EFFECT_BADGE
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 4,
+ "mVisualEffects_Badge");
+ // VISUAL_EFFECT_AMBIENT
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 5,
+ "mVisualEffects_Ambient");
+ // VISUAL_EFFECT_NOTIFICATION_LIST
+ setPolicyListValueDiff(aList, bList, expectedA, expectedB, 6,
+ "mVisualEffects_NotificationList");
+ // Assert that we've set every VisualeEffect enum value.
+ assertThat(Collections.frequency(aList, ZenPolicy.STATE_ALLOW))
+ .isEqualTo(ZenPolicy.NUM_VISUAL_EFFECTS);
+ } else {
+ // Any other lists that are added should be added to the diff.
+ fail("could not generate field diffs for policy list: " + f.getName());
+ }
+ }
+ }
+ }
+
+ // Helper function to create a diff in two list values at a given index, and record that
+ // diff's values in the associated expected maps under the provided field name.
+ private void setPolicyListValueDiff(List<Integer> aList, List<Integer> bList,
+ ArrayMap<String, Object> expectedA,
+ ArrayMap<String, Object> expectedB,
+ int index, String fieldName) {
+ aList.set(index, ZenPolicy.STATE_ALLOW);
+ expectedA.put(fieldName, ZenPolicy.STATE_ALLOW);
+ bList.set(index, ZenPolicy.STATE_DISALLOW);
+ expectedB.put(fieldName, ZenPolicy.STATE_DISALLOW);
+ }
+
// Generate a set of generic diffs for the specified two objects and the fields to generate
// diffs for, and store the results in the provided expectation maps to be able to check the
// output later.
@@ -420,6 +859,44 @@
expectedA.put(f.getName(), "string1");
f.set(b, "string2");
expectedB.put(f.getName(), "string2");
+ } else if (Set.class.equals(t)) {
+ Set<String> aSet = Set.of("effect1");
+ Set<String> bSet = Set.of("effect2");
+ f.set(a, aSet);
+ expectedA.put(f.getName(), aSet);
+ f.set(b, bSet);
+ expectedB.put(f.getName(), bSet);
+ } else if (ZenDeviceEffects.class.equals(t)) {
+ // Recurse into generating field diffs for ZenDeviceEffects.
+ ZenDeviceEffects effects1 = new ZenDeviceEffects.Builder().build();
+ ZenDeviceEffects effects2 = new ZenDeviceEffects.Builder().build();
+ // maps mapping field name -> expected output value as we set diffs
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(
+ ZenDeviceEffects.class, Collections.emptySet() /*no exempt fields*/, true);
+ generateFieldDiffs(effects1, effects2, fieldsForDiff, expectedFrom, expectedTo);
+ f.set(a, effects1);
+ expectedA.put(f.getName(), effects1);
+ f.set(b, effects2);
+ expectedB.put(f.getName(), effects2);
+ } else if (ZenPolicy.class.equals(t)) {
+ // Recurse into generating field diffs for ZenPolicy.
+ ZenPolicy policy1 = new ZenPolicy.Builder().build();
+ ZenPolicy policy2 = new ZenPolicy.Builder().build();
+ // maps mapping field name -> expected output value as we set diffs
+ ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
+ ArrayMap<String, Object> expectedTo = new ArrayMap<>();
+
+ List<Field> fieldsForDiff = getFieldsForDiffCheck(ZenPolicy.class,
+ Collections.emptySet(), false);
+ generateFieldDiffsForZenPolicy(policy1, policy2, fieldsForDiff, expectedFrom,
+ expectedTo);
+ f.set(a, policy1);
+ expectedA.put(f.getName(), policy1);
+ f.set(b, policy2);
+ expectedB.put(f.getName(), policy2);
} else {
// catch-all for other types: have the field be "added"
f.set(a, null);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
index b997f5d..a49f5a8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java
@@ -26,8 +26,6 @@
import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES;
import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY;
-import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
-import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
@@ -57,7 +55,6 @@
import androidx.test.filters.SmallTest;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.util.NotificationMessagingUtil;
import com.android.server.UiServiceTestCase;
@@ -188,49 +185,6 @@
}
@Test
- public void testSuppressDNDInfo_yes_VisEffectsAllowed() {
- NotificationRecord r = getNotificationRecord();
- when(r.getSbn().getPackageName()).thenReturn("android");
- when(r.getSbn().getId()).thenReturn(SystemMessage.NOTE_ZEN_UPGRADE);
- Policy policy = new Policy(0, 0, 0, Policy.getAllSuppressedVisualEffects()
- - SUPPRESSED_EFFECT_STATUS_BAR, 0);
-
- assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
- }
-
- @Test
- public void testSuppressDNDInfo_yes_WrongId() {
- NotificationRecord r = getNotificationRecord();
- when(r.getSbn().getPackageName()).thenReturn("android");
- when(r.getSbn().getId()).thenReturn(SystemMessage.NOTE_ACCOUNT_CREDENTIAL_PERMISSION);
- Policy policy = new Policy(0, 0, 0, Policy.getAllSuppressedVisualEffects(), 0);
-
- assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
- }
-
- @Test
- public void testSuppressDNDInfo_yes_WrongPackage() {
- NotificationRecord r = getNotificationRecord();
- when(r.getSbn().getPackageName()).thenReturn("android2");
- when(r.getSbn().getId()).thenReturn(SystemMessage.NOTE_ZEN_UPGRADE);
- Policy policy = new Policy(0, 0, 0, Policy.getAllSuppressedVisualEffects(), 0);
-
- assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
- }
-
- @Test
- public void testSuppressDNDInfo_no() {
- NotificationRecord r = getNotificationRecord();
- when(r.getSbn().getPackageName()).thenReturn("android");
- when(r.getSbn().getId()).thenReturn(SystemMessage.NOTE_ZEN_UPGRADE);
- Policy policy = new Policy(0, 0, 0, Policy.getAllSuppressedVisualEffects(), 0);
-
- assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
- assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_ALARMS, policy, r));
- assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_NO_INTERRUPTIONS, policy, r));
- }
-
- @Test
public void testSuppressAnything_yes_ZenModeOff() {
NotificationRecord r = getNotificationRecord();
when(r.getSbn().getPackageName()).thenReturn("bananas");
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index d4cba8d..294027b 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -169,7 +169,6 @@
import com.android.internal.R;
import com.android.internal.config.sysui.TestableFlagResolver;
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.os.AtomsProto;
@@ -747,54 +746,6 @@
}
@Test
- public void testZenUpgradeNotification() {
- /**
- * Commit a485ec65b5ba947d69158ad90905abf3310655cf disabled DND status change
- * notification on watches. So, assume that the device is not watch.
- */
- when(mContext.getPackageManager()).thenReturn(mPackageManager);
- when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false);
-
- // shows zen upgrade notification if stored settings says to shows,
- // zen has not been updated, boot is completed
- // and we're setting zen mode on
- Settings.Secure.putInt(mContentResolver, Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1);
- Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_SETTINGS_UPDATED, 0);
- mZenModeHelper.mIsSystemServicesReady = true;
- mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
- mZenModeHelper.setZenModeSetting(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
-
- verify(mNotificationManager, times(1)).notify(eq(ZenModeHelper.TAG),
- eq(SystemMessage.NOTE_ZEN_UPGRADE), any());
- assertEquals(0, Settings.Secure.getInt(mContentResolver,
- Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, -1));
- }
-
- @Test
- public void testNoZenUpgradeNotification() {
- // doesn't show upgrade notification if stored settings says don't show
- Settings.Secure.putInt(mContentResolver, Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
- Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_SETTINGS_UPDATED, 0);
- mZenModeHelper.mIsSystemServicesReady = true;
- mZenModeHelper.setZenModeSetting(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
-
- verify(mNotificationManager, never()).notify(eq(ZenModeHelper.TAG),
- eq(SystemMessage.NOTE_ZEN_UPGRADE), any());
- }
-
- @Test
- public void testNoZenUpgradeNotificationZenUpdated() {
- // doesn't show upgrade notification since zen was already updated
- Settings.Secure.putInt(mContentResolver, Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 0);
- Settings.Secure.putInt(mContentResolver, Settings.Secure.ZEN_SETTINGS_UPDATED, 1);
- mZenModeHelper.mIsSystemServicesReady = true;
- mZenModeHelper.setZenModeSetting(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
-
- verify(mNotificationManager, never()).notify(eq(ZenModeHelper.TAG),
- eq(SystemMessage.NOTE_ZEN_UPGRADE), any());
- }
-
- @Test
public void testZenSetInternalRinger_AllPriorityNotificationSoundsMuted() {
AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
mZenModeHelper.mAudioManager = mAudioManager;
@@ -3032,6 +2983,33 @@
}
@Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void updateAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
+ ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
+ sleepingRule.enabled = false;
+ sleepingRule.userModifiedFields = 0;
+ sleepingRule.name = "ZZZZZZZ...";
+ mZenModeHelper.mConfig.automaticRules.clear();
+ mZenModeHelper.mConfig.automaticRules.put(sleepingRule.id, sleepingRule);
+
+ AutomaticZenRule futureBedtime = new AutomaticZenRule.Builder("Bedtime (?)", CONDITION_ID)
+ .build();
+ String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(mPkg, futureBedtime,
+ ORIGIN_APP, "reason", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet())
+ .containsExactly(sleepingRule.id, bedtimeRuleId);
+
+ AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime (!)", CONDITION_ID)
+ .setType(TYPE_BEDTIME)
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(bedtimeRuleId, bedtime, ORIGIN_APP, "reason",
+ CUSTOM_PKG_UID);
+
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(bedtimeRuleId);
+ }
+
+ @Test
@EnableFlags(FLAG_MODES_API)
public void testSetManualZenMode() {
setupZenConfig();
diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
index 9b92ff4..3ea3235 100644
--- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java
@@ -23,6 +23,7 @@
import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP;
import static com.android.server.policy.PhoneWindowManager.DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT;
import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS;
@@ -32,6 +33,7 @@
import android.app.ActivityManager.RecentTaskInfo;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.content.ComponentName;
+import android.hardware.input.KeyGestureEvent;
import android.os.RemoteException;
import android.provider.Settings;
import android.view.Display;
@@ -236,6 +238,19 @@
}
@Test
+ public void stemDoubleKey_behaviorIsLaunchFitness_gestureEventFired() {
+ overrideBehavior(
+ STEM_PRIMARY_BUTTON_DOUBLE_PRESS, DOUBLE_PRESS_PRIMARY_LAUNCH_DEFAULT_FITNESS_APP);
+ setUpPhoneWindowManager(/* supportSettingsUpdate= */ true);
+
+ sendKey(KEYCODE_STEM_PRIMARY);
+ sendKey(KEYCODE_STEM_PRIMARY);
+
+ mPhoneWindowManager.assertKeyGestureEventSentToKeyGestureController(
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS);
+ }
+
+ @Test
public void stemTripleKey_EarlyShortPress_AllAppsThenBackToOriginalThenToggleA11y()
throws RemoteException {
overrideBehavior(STEM_PRIMARY_BUTTON_SHORT_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 1aa9087..a85f866 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -919,4 +919,9 @@
mTestLooper.dispatchAll();
Assert.assertEquals(expectEnabled, mIsTalkBackEnabled);
}
+
+ void assertKeyGestureEventSentToKeyGestureController(int gestureType) {
+ verify(mInputManagerInternal)
+ .handleKeyGestureInKeyGestureController(anyInt(), any(), anyInt(), eq(gestureType));
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index adc969c..72f4fa91 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -4881,6 +4881,25 @@
assertNotEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation());
}
+
+ @Test
+ @EnableCompatChanges({ActivityRecord.UNIVERSAL_RESIZABLE_BY_DEFAULT})
+ public void testUniversalResizeableByDefault() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT);
+ mDisplayContent.setIgnoreOrientationRequest(false);
+ setUpApp(mDisplayContent);
+ assertFalse(mActivity.isUniversalResizeable());
+
+ mDisplayContent.setIgnoreOrientationRequest(true);
+ final int swDp = mDisplayContent.getConfiguration().smallestScreenWidthDp;
+ if (swDp < WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP) {
+ final int height = 100 + (int) (mDisplayContent.getDisplayMetrics().density
+ * WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP);
+ resizeDisplay(mDisplayContent, 100 + height, height);
+ }
+ assertTrue(mActivity.isUniversalResizeable());
+ }
+
@Test
public void testClearSizeCompat_resetOverrideConfig() {
final int origDensity = 480;
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 2bb86bc..1a42e80 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -687,6 +687,7 @@
@Override
public int startRecognitionForService(ParcelUuid soundModelId, Bundle params,
ComponentName detectionService, SoundTrigger.RecognitionConfig config) {
+ final UserHandle userHandle = Binder.getCallingUserHandle();
mEventLogger.enqueue(new SessionEvent(Type.START_RECOGNITION_SERVICE,
getUuid(soundModelId)));
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
@@ -699,7 +700,7 @@
IRecognitionStatusCallback callback =
new RemoteSoundTriggerDetectionService(soundModelId.getUuid(), params,
- detectionService, Binder.getCallingUserHandle(), config);
+ detectionService, userHandle, config);
synchronized (mLock) {
SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 92effe0..ff966ae 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -2298,13 +2298,9 @@
*
* See {@link #getImei(int)} for details on the required permissions and behavior
* when the caller does not hold sufficient permissions.
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
@SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
public String getImei() {
return getImei(getSlotIndex());
}
@@ -2343,13 +2339,9 @@
* </ul>
*
* @param slotIndex of which IMEI is returned
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
@SuppressAutoDoc // No support for device / profile owner or carrier privileges (b/72967236).
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
public String getImei(int slotIndex) {
ITelephony telephony = getITelephony();
if (telephony == null) return null;
@@ -2366,11 +2358,7 @@
/**
* Returns the Type Allocation Code from the IMEI. Return null if Type Allocation Code is not
* available.
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
@Nullable
public String getTypeAllocationCode() {
return getTypeAllocationCode(getSlotIndex());
@@ -2381,11 +2369,7 @@
* available.
*
* @param slotIndex of which Type Allocation Code is returned
- *
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
*/
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
@Nullable
public String getTypeAllocationCode(int slotIndex) {
ITelephony telephony = getITelephony();
@@ -10613,20 +10597,31 @@
return null;
}
- /** @hide */
+ /**
+ * Get the names of packages with carrier privileges for the current subscription.
+ *
+ * @throws UnsupportedOperationException If the device does not have {@link
+ * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}
+ * @hide
+ */
+ @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API)
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- public List<String> getPackagesWithCarrierPrivileges() {
+ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
+ @NonNull
+ public Set<String> getPackagesWithCarrierPrivileges() {
+ final Set<String> result = new HashSet<>();
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
- return telephony.getPackagesWithCarrierPrivileges(getPhoneId());
+ result.addAll(telephony.getPackagesWithCarrierPrivileges(getPhoneId()));
}
} catch (RemoteException ex) {
Rlog.e(TAG, "getPackagesWithCarrierPrivileges RemoteException", ex);
} catch (NullPointerException ex) {
Rlog.e(TAG, "getPackagesWithCarrierPrivileges NPE", ex);
}
- return Collections.EMPTY_LIST;
+ return result;
}
/**
@@ -19367,12 +19362,9 @@
* </ul>
*
* @return Primary IMEI of type string
- * @throws UnsupportedOperationException If the device does not have
- * {@link PackageManager#FEATURE_TELEPHONY_GSM}.
* @throws SecurityException if the caller does not have the required permission/privileges
*/
@NonNull
- @RequiresFeature(PackageManager.FEATURE_TELEPHONY_GSM)
public String getPrimaryImei() {
try {
ITelephony telephony = getITelephony();
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index bd5c759..49ca6f3 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -249,6 +249,13 @@
public static final String KEY_PROVISION_SATELLITE_TOKENS = "provision_satellite";
/**
+ * Bundle key to get the response from
+ * {@link #deprovisionSatellite(List, Executor, OutcomeReceiver)}.
+ * @hide
+ */
+ public static final String KEY_DEPROVISION_SATELLITE_TOKENS = "deprovision_satellite";
+
+ /**
* The request was successfully processed.
*/
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -2791,6 +2798,61 @@
}
}
+ /**
+ * Deliver the list of deprovisioned satellite subscriber infos.
+ *
+ * @param list The list of deprovisioned satellite subscriber infos.
+ * @param executor The executor on which the callback will be called.
+ * @param callback The callback object to which the result will be delivered.
+ *
+ * @throws SecurityException if the caller doesn't have required permission.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
+ @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
+ public void deprovisionSatellite(@NonNull List<SatelliteSubscriberInfo> list,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ ResultReceiver receiver = new ResultReceiver(null) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == SATELLITE_RESULT_SUCCESS) {
+ if (resultData.containsKey(KEY_DEPROVISION_SATELLITE_TOKENS)) {
+ boolean isUpdated =
+ resultData.getBoolean(KEY_DEPROVISION_SATELLITE_TOKENS);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onResult(isUpdated)));
+ } else {
+ loge("KEY_DEPROVISION_SATELLITE_TOKENS does not exist.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(
+ SATELLITE_RESULT_REQUEST_FAILED))));
+ }
+ } else {
+ executor.execute(() -> Binder.withCleanCallingIdentity(() ->
+ callback.onError(new SatelliteException(resultCode))));
+ }
+ }
+ };
+ telephony.deprovisionSatellite(list, receiver);
+ } else {
+ loge("deprovisionSatellite() invalid telephony");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ } catch (RemoteException ex) {
+ loge("deprovisionSatellite() RemoteException: " + ex);
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
+ }
+ }
+
@Nullable
private static ITelephony getITelephony() {
ITelephony binder = ITelephony.Stub.asInterface(TelephonyFrameworkInitializer
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 3161d17..61f0146 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3444,4 +3444,15 @@
*/
boolean overrideCarrierRoamingNtnEligibilityChanged(
in boolean status, in boolean resetRequired);
+
+ /**
+ * Deliver the list of deprovisioned satellite subscriber infos.
+ *
+ * @param list The list of deprovisioned satellite subscriber infos.
+ * @param result The result receiver that returns whether deliver success or fail.
+ * @hide
+ */
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
+ void deprovisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
}
diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
index c61a250..9f4df90 100644
--- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
+++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt
@@ -39,6 +39,7 @@
import junit.framework.Assert.fail
import org.hamcrest.Matchers.allOf
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -107,6 +108,7 @@
parser = InputJsonParser(instrumentation.context)
}
+ @Ignore("b/366602644")
@Test
fun testEvemuRecording() {
VirtualDisplayActivityScenario.AutoClose<CaptureEventActivity>(
diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
index 9d56a92..8ecddaa 100644
--- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
+++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java
@@ -16,6 +16,8 @@
package com.android.internal.protolog;
+import static org.junit.Assert.assertThrows;
+
import android.platform.test.annotations.Presubmit;
import com.android.internal.protolog.common.IProtoLogGroup;
@@ -44,8 +46,29 @@
.containsExactly(TEST_GROUP_1, TEST_GROUP_2);
}
+ @Test
+ public void throwOnRegisteringDuplicateGroup() {
+ final var assertion = assertThrows(RuntimeException.class,
+ () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2));
+
+ Truth.assertThat(assertion).hasMessageThat().contains("" + TEST_GROUP_1.getId());
+ Truth.assertThat(assertion).hasMessageThat().contains("duplicate");
+ }
+
+ @Test
+ public void throwOnRegisteringGroupsWithIdCollisions() {
+ final var assertion = assertThrows(RuntimeException.class,
+ () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_WITH_COLLISION, TEST_GROUP_2));
+
+ Truth.assertThat(assertion).hasMessageThat()
+ .contains("" + TEST_GROUP_WITH_COLLISION.getId());
+ Truth.assertThat(assertion).hasMessageThat().contains("collision");
+ }
+
private static final IProtoLogGroup TEST_GROUP_1 = new ProtoLogGroup("TEST_TAG_1", 1);
private static final IProtoLogGroup TEST_GROUP_2 = new ProtoLogGroup("TEST_TAG_2", 2);
+ private static final IProtoLogGroup TEST_GROUP_WITH_COLLISION =
+ new ProtoLogGroup("TEST_TAG_WITH_COLLISION", 1);
private static class ProtoLogGroup implements IProtoLogGroup {
private final boolean mEnabled;
diff --git a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
index 81814b6..7bc9970 100644
--- a/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnTransportInfoTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
import android.net.NetworkCapabilities;
import android.net.wifi.WifiConfiguration;
@@ -39,6 +40,7 @@
private static final int SUB_ID = 1;
private static final int NETWORK_ID = 5;
private static final int MIN_UDP_PORT_4500_NAT_TIMEOUT = 120;
+ private static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_INVALID = 119;
private static final WifiInfo WIFI_INFO =
new WifiInfo.Builder().setNetworkId(NETWORK_ID).build();
@@ -48,6 +50,27 @@
new VcnTransportInfo(WIFI_INFO, MIN_UDP_PORT_4500_NAT_TIMEOUT);
@Test
+ public void testBuilder() {
+ final VcnTransportInfo transportInfo =
+ new VcnTransportInfo.Builder()
+ .setMinUdpPort4500NatTimeoutSeconds(MIN_UDP_PORT_4500_NAT_TIMEOUT)
+ .build();
+
+ assertEquals(
+ MIN_UDP_PORT_4500_NAT_TIMEOUT, transportInfo.getMinUdpPort4500NatTimeoutSeconds());
+ }
+
+ @Test
+ public void testBuilder_withInvalidNatTimeout() {
+ try {
+ new VcnTransportInfo.Builder()
+ .setMinUdpPort4500NatTimeoutSeconds(MIN_UDP_PORT_4500_NAT_TIMEOUT_INVALID);
+ fail("Expected to fail due to invalid NAT timeout");
+ } catch (Exception expected) {
+ }
+ }
+
+ @Test
public void testGetWifiInfo() {
assertEquals(WIFI_INFO, WIFI_UNDERLYING_INFO.getWifiInfo());
diff --git a/tests/vcn/java/android/net/vcn/VcnUtilsTest.java b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
new file mode 100644
index 0000000..3ce6c8f
--- /dev/null
+++ b/tests/vcn/java/android/net/vcn/VcnUtilsTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2021 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.net.vcn;
+
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.wifi.WifiInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class VcnUtilsTest {
+ private static final int SUB_ID = 1;
+
+ private static final WifiInfo WIFI_INFO = new WifiInfo.Builder().build();
+ private static final TelephonyNetworkSpecifier TEL_NETWORK_SPECIFIER =
+ new TelephonyNetworkSpecifier.Builder().setSubscriptionId(SUB_ID).build();
+ private static final VcnTransportInfo VCN_TRANSPORT_INFO =
+ new VcnTransportInfo.Builder().build();
+
+ private ConnectivityManager mMockConnectivityManager;
+ private Network mMockWifiNetwork;
+ private Network mMockCellNetwork;
+
+ private NetworkCapabilities mVcnCapsWithUnderlyingWifi;
+ private NetworkCapabilities mVcnCapsWithUnderlyingCell;
+
+ @Before
+ public void setUp() {
+ mMockConnectivityManager = mock(ConnectivityManager.class);
+
+ mMockWifiNetwork = mock(Network.class);
+ mVcnCapsWithUnderlyingWifi = newVcnCaps(VCN_TRANSPORT_INFO, mMockWifiNetwork);
+ final NetworkCapabilities wifiCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .setTransportInfo(WIFI_INFO)
+ .build();
+ when(mMockConnectivityManager.getNetworkCapabilities(mMockWifiNetwork))
+ .thenReturn(wifiCaps);
+
+ mMockCellNetwork = mock(Network.class);
+ mVcnCapsWithUnderlyingCell = newVcnCaps(VCN_TRANSPORT_INFO, mMockCellNetwork);
+ final NetworkCapabilities cellCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .setNetworkSpecifier(TEL_NETWORK_SPECIFIER)
+ .build();
+ when(mMockConnectivityManager.getNetworkCapabilities(mMockCellNetwork))
+ .thenReturn(cellCaps);
+ }
+
+ private static NetworkCapabilities newVcnCaps(
+ VcnTransportInfo vcnTransportInfo, Network underlyingNetwork) {
+ return new NetworkCapabilities.Builder()
+ .setTransportInfo(vcnTransportInfo)
+ .setUnderlyingNetworks(Collections.singletonList(underlyingNetwork))
+ .build();
+ }
+
+ @Test
+ public void getWifiInfoFromVcnCaps() {
+ assertEquals(
+ WIFI_INFO,
+ VcnUtils.getWifiInfoFromVcnCaps(
+ mMockConnectivityManager, mVcnCapsWithUnderlyingWifi));
+ }
+
+ @Test
+ public void getWifiInfoFromVcnCaps_onVcnWithUnderlyingCell() {
+ assertNull(
+ VcnUtils.getWifiInfoFromVcnCaps(
+ mMockConnectivityManager, mVcnCapsWithUnderlyingCell));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps() {
+ assertEquals(
+ SUB_ID,
+ VcnUtils.getSubIdFromVcnCaps(mMockConnectivityManager, mVcnCapsWithUnderlyingCell));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps_onVcnWithUnderlyingWifi() {
+ assertEquals(
+ INVALID_SUBSCRIPTION_ID,
+ VcnUtils.getSubIdFromVcnCaps(mMockConnectivityManager, mVcnCapsWithUnderlyingWifi));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps_onNonVcnNetwork() {
+ assertEquals(
+ INVALID_SUBSCRIPTION_ID,
+ VcnUtils.getSubIdFromVcnCaps(
+ mMockConnectivityManager, new NetworkCapabilities.Builder().build()));
+ }
+
+ @Test
+ public void getSubIdFromVcnCaps_withMultipleUnderlyingNetworks() {
+ final NetworkCapabilities vcnCaps =
+ new NetworkCapabilities.Builder(mVcnCapsWithUnderlyingCell)
+ .setUnderlyingNetworks(
+ Arrays.asList(
+ new Network[] {mMockCellNetwork, mock(Network.class)}))
+ .build();
+ assertEquals(SUB_ID, VcnUtils.getSubIdFromVcnCaps(mMockConnectivityManager, vcnCaps));
+ }
+}
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index b5cc553..f1f74bc 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -206,7 +206,7 @@
.getAllSubscriptionInfoList();
doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
- setPrivilegedPackagesForMock(Collections.singletonList(PACKAGE_NAME));
+ setPrivilegedPackagesForMock(Collections.singleton(PACKAGE_NAME));
}
private IntentFilter getIntentFilter() {
@@ -293,7 +293,7 @@
Collections.singletonMap(TEST_SUBSCRIPTION_ID_1, TEST_CARRIER_CONFIG_WRAPPER));
}
- private void setPrivilegedPackagesForMock(@NonNull List<String> privilegedPackages) {
+ private void setPrivilegedPackagesForMock(@NonNull Set<String> privilegedPackages) {
doReturn(privilegedPackages).when(mTelephonyManager).getPackagesWithCarrierPrivileges();
}
@@ -390,7 +390,7 @@
@Test
public void testOnSubscriptionsChangedFired_onActiveSubIdsChanged() throws Exception {
setupReadySubIds();
- setPrivilegedPackagesForMock(Collections.emptyList());
+ setPrivilegedPackagesForMock(Collections.emptySet());
doReturn(TEST_SUBSCRIPTION_ID_2).when(mDeps).getActiveDataSubscriptionId();
final ActiveDataSubscriptionIdListener listener = getActiveDataSubscriptionIdListener();
@@ -411,7 +411,7 @@
public void testOnSubscriptionsChangedFired_WithReadySubidsNoPrivilegedPackages()
throws Exception {
setupReadySubIds();
- setPrivilegedPackagesForMock(Collections.emptyList());
+ setPrivilegedPackagesForMock(Collections.emptySet());
final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
listener.onSubscriptionsChanged();
@@ -567,7 +567,7 @@
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
// Simulate a loss of carrier privileges
- setPrivilegedPackagesForMock(Collections.emptyList());
+ setPrivilegedPackagesForMock(Collections.emptySet());
listener.onSubscriptionsChanged();
mTestLooper.dispatchAll();
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index da092f4..fb576df 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -110,7 +110,7 @@
std::optional<OverlayableItem> overlayable_item;
std::optional<StagedId> staged_alias;
std::optional<FeatureFlagAttribute> flag;
- FlagStatus flag_status;
+ FlagStatus flag_status = FlagStatus::NoFlag;
std::string comment;
std::unique_ptr<Value> value;
diff --git a/tools/processors/property_cache/OWNERS b/tools/processors/property_cache/OWNERS
new file mode 100644
index 0000000..78650168
--- /dev/null
+++ b/tools/processors/property_cache/OWNERS
@@ -0,0 +1,3 @@
+include /ACTIVITY_MANAGER_OWNERS
+include /BROADCASTS_OWNERS
+include /MULTIUSER_OWNERS
\ No newline at end of file