Merge "Fix incorrect placement of HUN icon in statusbar" into main
diff --git a/Android.bp b/Android.bp
index b5f7e99..49256dd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -631,6 +631,7 @@
name: "android-non-updatable-stub-sources",
srcs: [
":framework-mime-sources", // mimemap builds separately but has no separate droidstubs.
+ ":framework-minus-apex-aconfig-srcjars",
":framework-non-updatable-sources",
":opt-telephony-srcs",
":opt-net-voip-srcs",
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 5dc994e..a92a01f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -1265,6 +1265,7 @@
/** @hide */
@NonNull
+ @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
public Builder setBias(int bias) {
mBias = bias;
return this;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 592aff8..1287cb4 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -202,6 +202,15 @@
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
static final long REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS = 271850009L;
+ /**
+ * Throw an exception when biases are set by an unsupported client.
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static final long THROW_ON_UNSUPPORTED_BIAS_USAGE = 300477393L;
+
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public static Clock sSystemClock = Clock.systemUTC();
@@ -4331,6 +4340,24 @@
}
}
+ private JobInfo enforceBuilderApiPermissions(int uid, int pid, JobInfo job) {
+ if (job.getBias() != JobInfo.BIAS_DEFAULT
+ && !hasPermission(uid, pid, Manifest.permission.UPDATE_DEVICE_STATS)) {
+ if (CompatChanges.isChangeEnabled(THROW_ON_UNSUPPORTED_BIAS_USAGE, uid)) {
+ throw new SecurityException("Apps may not call setBias()");
+ } else {
+ // We can't throw the exception. Log the issue and modify the job to remove
+ // the invalid value.
+ Slog.w(TAG, "Uid " + uid + " set bias on its job");
+ return new JobInfo.Builder(job)
+ .setBias(JobInfo.BIAS_DEFAULT)
+ .build(false, false);
+ }
+ }
+
+ return job;
+ }
+
private boolean canPersistJobs(int pid, int uid) {
// Persisting jobs is tantamount to running at boot, so we permit
// it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
@@ -4512,6 +4539,8 @@
namespace = validateNamespace(namespace);
+ job = enforceBuilderApiPermissions(uid, pid, job);
+
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
@@ -4543,6 +4572,8 @@
namespace = validateNamespace(namespace);
+ job = enforceBuilderApiPermissions(uid, pid, job);
+
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
@@ -4582,6 +4613,8 @@
namespace = validateNamespace(namespace);
+ job = enforceBuilderApiPermissions(callerUid, callerPid, job);
+
final long ident = Binder.clearCallingIdentity();
try {
return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
diff --git a/api/Android.bp b/api/Android.bp
index 222275f..d11ea7b 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -74,7 +74,6 @@
"framework-configinfrastructure",
"framework-connectivity",
"framework-connectivity-t",
- "framework-crashrecovery",
"framework-devicelock",
"framework-graphics",
"framework-healthfitness",
@@ -97,7 +96,6 @@
system_server_classpath: [
"service-art",
"service-configinfrastructure",
- "service-crashrecovery",
"service-healthfitness",
"service-media-s",
"service-permission",
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index d566552..5688b96 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -29,9 +29,6 @@
droidstubs {
name: "api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
@@ -91,9 +88,6 @@
droidstubs {
name: "system-api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
@@ -134,9 +128,6 @@
droidstubs {
name: "test-api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
@@ -184,9 +175,6 @@
droidstubs {
name: "module-lib-api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
diff --git a/boot/Android.bp b/boot/Android.bp
index b33fab6..8a3d35e 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -84,10 +84,6 @@
module: "com.android.conscrypt-bootclasspath-fragment",
},
{
- apex: "com.android.crashrecovery",
- module: "com.android.crashrecovery-bootclasspath-fragment",
- },
- {
apex: "com.android.devicelock",
module: "com.android.devicelock-bootclasspath-fragment",
},
diff --git a/core/api/current.txt b/core/api/current.txt
index b001379..31fb78a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -18192,12 +18192,16 @@
field public static final int D_16 = 48; // 0x30
field public static final int D_24 = 49; // 0x31
field public static final int D_FP32 = 51; // 0x33
+ field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int RGBA_10101010 = 59; // 0x3b
field public static final int RGBA_1010102 = 43; // 0x2b
field public static final int RGBA_8888 = 1; // 0x1
field public static final int RGBA_FP16 = 22; // 0x16
field public static final int RGBX_8888 = 2; // 0x2
field public static final int RGB_565 = 4; // 0x4
field public static final int RGB_888 = 3; // 0x3
+ field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int RG_1616_UINT = 58; // 0x3a
+ field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int R_16_UINT = 57; // 0x39
+ field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int R_8 = 56; // 0x38
field public static final int S_UI8 = 53; // 0x35
field public static final long USAGE_COMPOSER_OVERLAY = 2048L; // 0x800L
field public static final long USAGE_CPU_READ_OFTEN = 3L; // 0x3L
@@ -28487,6 +28491,7 @@
method @NonNull public long[] getRetryIntervalsMillis();
method @NonNull public java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate> getVcnUnderlyingNetworkPriorities();
method public boolean hasGatewayOption(int);
+ method @FlaggedApi("android.net.vcn.safe_mode_config") public boolean isSafeModeEnabled();
field public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; // 0x0
}
@@ -28495,6 +28500,7 @@
method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addExposedCapability(int);
method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addGatewayOption(int);
method @NonNull public android.net.vcn.VcnGatewayConnectionConfig build();
+ method @FlaggedApi("android.net.vcn.safe_mode_config") @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder enableSafeMode(boolean);
method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeExposedCapability(int);
method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeGatewayOption(int);
method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMaxMtu(@IntRange(from=0x500) int);
@@ -32198,7 +32204,7 @@
field public static final int S_V2 = 32; // 0x20
field public static final int TIRAMISU = 33; // 0x21
field public static final int UPSIDE_DOWN_CAKE = 34; // 0x22
- field public static final int VANILLA_ICE_CREAM = 10000; // 0x2710
+ field @FlaggedApi("android.os.android_os_build_vanilla_ice_cream") public static final int VANILLA_ICE_CREAM = 10000; // 0x2710
}
public final class Bundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable {
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 5bbff0b..0737496 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -15,7 +15,7 @@
@UiContext public class Activity extends android.view.ContextThemeWrapper implements android.content.ComponentCallbacks2 android.view.KeyEvent.Callback android.view.LayoutInflater.Factory2 android.view.View.OnCreateContextMenuListener android.view.Window.Callback {
method public final boolean addDumpable(@NonNull android.util.Dumpable);
- method public final boolean isResumed();
+ method @FlaggedApi("android.nfc.enable_nfc_mainline") public final boolean isResumed();
}
public class ActivityManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 6062f79..0ad73af 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -9661,6 +9661,7 @@
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable();
method @FlaggedApi("android.nfc.enable_nfc_reader_option") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableReaderOption(boolean);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean);
+ method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getAdapterState();
method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public java.util.Map<java.lang.String,java.lang.Boolean> getTagIntentAppPreferenceForUser(int);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn();
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOnSupported();
@@ -9671,6 +9672,7 @@
method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderMode(boolean);
method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean);
method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener);
+ field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC";
field public static final int TAG_INTENT_APP_PREF_RESULT_PACKAGE_NOT_FOUND = -1; // 0xffffffff
field public static final int TAG_INTENT_APP_PREF_RESULT_SUCCESS = 0; // 0x0
field public static final int TAG_INTENT_APP_PREF_RESULT_UNAVAILABLE = -2; // 0xfffffffe
@@ -9732,6 +9734,10 @@
field @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.ApduServiceInfo> CREATOR;
}
+ public final class CardEmulation {
+ method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int);
+ }
+
@FlaggedApi("android.nfc.enable_nfc_mainline") public final class NfcFServiceInfo implements android.os.Parcelable {
ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public NfcFServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 6cda015..74a4440 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -866,6 +866,14 @@
}
+package android.companion.virtual {
+
+ public final class VirtualDeviceManager {
+ method @FlaggedApi("android.companion.virtual.flags.interactive_screen_mirror") public boolean isVirtualDeviceOwnedMirrorDisplay(int);
+ }
+
+}
+
package android.content {
public final class AttributionSource implements android.os.Parcelable {
@@ -1794,8 +1802,8 @@
public class AudioManager {
method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String);
- method @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean enterAudioFocusFreezeForTest(@NonNull java.util.List<java.lang.Integer>);
- method @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean exitAudioFocusFreezeForTest();
+ method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean enterAudioFocusFreezeForTest(@NonNull java.util.List<java.lang.Integer>);
+ method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED") public boolean exitAudioFocusFreezeForTest();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void forceComputeCsdOnAllDevices(boolean);
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public void forceUseFrameworkMel(boolean);
method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
@@ -1803,9 +1811,9 @@
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) public float getCsd();
method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int);
method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes);
- method @NonNull @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public java.util.List<java.lang.Integer> getFocusDuckedUidsForTest();
- method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusFadeOutDurationForTest();
- method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusUnmuteDelayAfterFadeOutForTest();
+ method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @NonNull @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public java.util.List<java.lang.Integer> getFocusDuckedUidsForTest();
+ method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusFadeOutDurationForTest();
+ method @FlaggedApi("com.android.media.audio.flags.focus_freeze_test_api") @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFocusUnmuteDelayAfterFadeOutForTest();
method @Nullable public static android.media.AudioHalVersionInfo getHalVersion();
method public static final int[] getPublicStreamTypes();
method @NonNull public java.util.List<java.lang.Integer> getReportedSurroundFormats();
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index e51a41e8..be433d2 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -9072,6 +9072,7 @@
* @hide
*/
@UnsupportedAppUsage
+ @FlaggedApi(android.nfc.Flags.FLAG_ENABLE_NFC_MAINLINE)
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public final boolean isResumed() {
return mResumed;
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index e12181a..3b6ea14 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -135,6 +135,7 @@
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
+import android.os.IBinderCallback;
import android.os.ICancellationSignal;
import android.os.LocaleList;
import android.os.Looper;
@@ -7274,6 +7275,18 @@
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
+
+ // Set binder transaction callback after finishing bindApplication
+ Binder.setTransactionCallback(new IBinderCallback() {
+ @Override
+ public void onTransactionError(int pid, int code, int flags, int err) {
+ try {
+ mgr.frozenBinderTransactionDetected(pid, code, flags, err);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ });
}
@UnsupportedAppUsage
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index d15c79f..24cb9ea 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -477,6 +477,16 @@
*/
public static final int SUBREASON_OOM_KILL = 30;
+ /**
+ * The process was killed because its async kernel binder buffer is running out
+ * while being frozen.
+ * this would be set only when the reason is {@link #REASON_FREEZER}.
+ *
+ * For internal use only.
+ * @hide
+ */
+ public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31;
+
// If there is any OEM code which involves additional app kill reasons, it should
// be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
@@ -654,6 +664,7 @@
SUBREASON_UNDELIVERED_BROADCAST,
SUBREASON_EXCESSIVE_BINDER_OBJECTS,
SUBREASON_OOM_KILL,
+ SUBREASON_FREEZER_BINDER_ASYNC_FULL,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SubReason {}
@@ -1383,6 +1394,8 @@
return "EXCESSIVE BINDER OBJECTS";
case SUBREASON_OOM_KILL:
return "OOM KILL";
+ case SUBREASON_FREEZER_BINDER_ASYNC_FULL:
+ return "FREEZER BINDER ASYNC FULL";
default:
return "UNKNOWN";
}
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 03baf26..520bf7d 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -939,4 +939,14 @@
int[] getUidFrozenState(in int[] uids);
int checkPermissionForDevice(in String permission, int pid, int uid, int deviceId);
+
+ /**
+ * Notify AMS about binder transactions to frozen apps.
+ *
+ * @param debugPid The binder transaction sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err);
}
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index b665036..0493312 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -122,4 +122,10 @@
* {@code android.media.AudioManager.SystemSoundEffect}
*/
void playSoundEffect(int deviceId, int effectType);
+
+ /**
+ * Returns whether the given display is an auto-mirror display owned by a virtual
+ * device.
+ */
+ boolean isVirtualDeviceOwnedMirrorDisplay(int displayId);
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 2569366..d40a591 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -28,6 +28,7 @@
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.companion.AssociationInfo;
@@ -436,6 +437,25 @@
}
/**
+ * Returns whether the given display is an auto-mirror display owned by a virtual device.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR)
+ @TestApi
+ public boolean isVirtualDeviceOwnedMirrorDisplay(int displayId) {
+ if (mService == null) {
+ Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service.");
+ return false;
+ }
+ try {
+ return mService.isVirtualDeviceOwnedMirrorDisplay(displayId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* A representation of a virtual device.
*
* <p>A virtual device can have its own virtual displays, audio input/output, sensors, etc.
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 21427ac..f380963 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -37,10 +37,17 @@
}
flag {
- name: "virtual_camera"
- namespace: "virtual_devices"
- description: "Enable Virtual Camera"
- bug: "270352264"
+ name: "virtual_camera"
+ namespace: "virtual_devices"
+ description: "Enable Virtual Camera"
+ bug: "270352264"
+}
+
+flag {
+ name: "stream_camera"
+ namespace: "virtual_devices"
+ description: "Enable streaming camera to Virtual Devices"
+ bug: "291740640"
}
flag {
@@ -56,3 +63,10 @@
description: "Enable express metrics in VDM"
bug: "307297730"
}
+
+flag {
+ name: "interactive_screen_mirror"
+ namespace: "virtual_devices"
+ description: "Enable interactive screen mirroring using Virtual Devices"
+ bug: "292212199"
+}
diff --git a/core/java/android/hardware/CameraSessionStats.java b/core/java/android/hardware/CameraSessionStats.java
index b1d6ac4..32938ff 100644
--- a/core/java/android/hardware/CameraSessionStats.java
+++ b/core/java/android/hardware/CameraSessionStats.java
@@ -65,6 +65,7 @@
private String mUserTag;
private int mVideoStabilizationMode;
private boolean mUsedUltraWide;
+ private boolean mUsedZoomOverride;
private int mSessionIndex;
private CameraExtensionSessionStats mCameraExtensionSessionStats;
@@ -84,6 +85,7 @@
mStreamStats = new ArrayList<CameraStreamStats>();
mVideoStabilizationMode = -1;
mUsedUltraWide = false;
+ mUsedZoomOverride = false;
mSessionIndex = 0;
mCameraExtensionSessionStats = new CameraExtensionSessionStats();
}
@@ -106,6 +108,7 @@
mStreamStats = new ArrayList<CameraStreamStats>();
mVideoStabilizationMode = -1;
mUsedUltraWide = false;
+ mUsedZoomOverride = false;
mSessionIndex = sessionIdx;
mCameraExtensionSessionStats = new CameraExtensionSessionStats();
}
@@ -152,6 +155,7 @@
dest.writeString(mUserTag);
dest.writeInt(mVideoStabilizationMode);
dest.writeBoolean(mUsedUltraWide);
+ dest.writeBoolean(mUsedZoomOverride);
dest.writeInt(mSessionIndex);
mCameraExtensionSessionStats.writeToParcel(dest, 0);
}
@@ -180,6 +184,7 @@
mVideoStabilizationMode = in.readInt();
mUsedUltraWide = in.readBoolean();
+ mUsedZoomOverride = in.readBoolean();
mSessionIndex = in.readInt();
mCameraExtensionSessionStats = CameraExtensionSessionStats.CREATOR.createFromParcel(in);
@@ -257,6 +262,10 @@
return mUsedUltraWide;
}
+ public boolean getUsedZoomOverride() {
+ return mUsedZoomOverride;
+ }
+
public int getSessionIndex() {
return mSessionIndex;
}
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index 5ff0e7a..e714887 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -16,6 +16,7 @@
package android.hardware;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.LongDef;
@@ -65,6 +66,10 @@
DS_FP32UI8,
S_UI8,
YCBCR_P010,
+ R_8,
+ R_16_UINT,
+ RG_1616_UINT,
+ RGBA_10101010,
})
public @interface Format {
}
@@ -105,7 +110,19 @@
* followed by a Wx(H/2) CbCr plane. Each sample is represented by a 16-bit
* little-endian value, with the lower 6 bits set to zero.
*/
- public static final int YCBCR_P010 = 0x36;
+ public static final int YCBCR_P010 = 0x36;
+ /** Format: 8 bits red */
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
+ public static final int R_8 = 0x38;
+ /** Format: 16 bits red */
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
+ public static final int R_16_UINT = 0x39;
+ /** Format: 16 bits each red, green */
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
+ public static final int RG_1616_UINT = 0x3a;
+ /** Format: 10 bits each red, green, blue, alpha */
+ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
+ public static final int RGBA_10101010 = 0x3b;
// Note: do not rename, this field is used by native code
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index 66e3c28..779a8db 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -494,7 +494,6 @@
* Check whether safe mode is enabled
*
* @see Builder#enableSafeMode(boolean)
- * @hide
*/
@FlaggedApi(FLAG_SAFE_MODE_CONFIG)
public boolean isSafeModeEnabled() {
@@ -824,7 +823,6 @@
* networks.
*
* @param enabled whether safe mode should be enabled. Defaults to {@code true}
- * @hide
*/
@FlaggedApi(FLAG_SAFE_MODE_CONFIG)
@NonNull
diff --git a/core/java/android/nfc/INfcCardEmulation.aidl b/core/java/android/nfc/INfcCardEmulation.aidl
index 53843fe..c7b3b2c 100644
--- a/core/java/android/nfc/INfcCardEmulation.aidl
+++ b/core/java/android/nfc/INfcCardEmulation.aidl
@@ -40,5 +40,6 @@
boolean unsetPreferredService();
boolean supportsAidPrefixRegistration();
ApduServiceInfo getPreferredPaymentService(int userHandle);
+ boolean setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status);
boolean isDefaultPaymentRegistered();
}
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index 4a7bd3f..c897595 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -378,6 +378,8 @@
* <p>An external NFC field detected when device locked and SecureNfc enabled.
* @hide
*/
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC =
"android.nfc.action.REQUIRE_UNLOCK_FOR_NFC";
@@ -944,7 +946,8 @@
*
* @hide
*/
- @UnsupportedAppUsage
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
public int getAdapterState() {
try {
return sService.getState();
diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
index 665b753..9cf8c4d 100644
--- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -127,6 +127,11 @@
private final String mSettingsActivityName;
/**
+ * State of the service for CATEGORY_OTHER selection
+ */
+ private boolean mOtherServiceSelectionState;
+
+ /**
* @hide
*/
public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
@@ -134,8 +139,21 @@
boolean requiresUnlock, int bannerResource, int uid,
String settingsActivityName, String offHost, String staticOffHost) {
this(info, onHost, description, staticAidGroups, dynamicAidGroups,
+ requiresUnlock, bannerResource, uid, settingsActivityName,
+ offHost, staticOffHost, false);
+ }
+
+ /**
+ * @hide
+ */
+ public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
+ List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
+ boolean requiresUnlock, int bannerResource, int uid,
+ String settingsActivityName, String offHost, String staticOffHost,
+ boolean isSelected) {
+ this(info, onHost, description, staticAidGroups, dynamicAidGroups,
requiresUnlock, onHost ? true : false, bannerResource, uid,
- settingsActivityName, offHost, staticOffHost);
+ settingsActivityName, offHost, staticOffHost, isSelected);
}
/**
@@ -144,7 +162,7 @@
public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
boolean requiresUnlock, boolean requiresScreenOn, int bannerResource, int uid,
- String settingsActivityName, String offHost, String staticOffHost) {
+ String settingsActivityName, String offHost, String staticOffHost, boolean isSelected) {
this.mService = info;
this.mDescription = description;
this.mStaticAidGroups = new HashMap<String, AidGroup>();
@@ -163,6 +181,8 @@
this.mBannerResourceId = bannerResource;
this.mUid = uid;
this.mSettingsActivityName = settingsActivityName;
+ this.mOtherServiceSelectionState = isSelected;
+
}
/**
@@ -351,6 +371,9 @@
}
// Set uid
mUid = si.applicationInfo.uid;
+
+ mOtherServiceSelectionState = false; // support other category
+
}
/**
@@ -720,43 +743,47 @@
dest.writeInt(mBannerResourceId);
dest.writeInt(mUid);
dest.writeString(mSettingsActivityName);
+
+ dest.writeInt(mOtherServiceSelectionState ? 1 : 0);
};
@FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
public static final @NonNull Parcelable.Creator<ApduServiceInfo> CREATOR =
new Parcelable.Creator<ApduServiceInfo>() {
- @Override
- public ApduServiceInfo createFromParcel(Parcel source) {
- ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source);
- String description = source.readString();
- boolean onHost = source.readInt() != 0;
- String offHostName = source.readString();
- String staticOffHostName = source.readString();
- ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>();
- int numStaticGroups = source.readInt();
- if (numStaticGroups > 0) {
- source.readTypedList(staticAidGroups, AidGroup.CREATOR);
- }
- ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>();
- int numDynamicGroups = source.readInt();
- if (numDynamicGroups > 0) {
- source.readTypedList(dynamicAidGroups, AidGroup.CREATOR);
- }
- boolean requiresUnlock = source.readInt() != 0;
- boolean requiresScreenOn = source.readInt() != 0;
- int bannerResource = source.readInt();
- int uid = source.readInt();
- String settingsActivityName = source.readString();
- return new ApduServiceInfo(info, onHost, description, staticAidGroups,
- dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid,
- settingsActivityName, offHostName, staticOffHostName);
- }
+ @Override
+ public ApduServiceInfo createFromParcel(Parcel source) {
+ ResolveInfo info = ResolveInfo.CREATOR.createFromParcel(source);
+ String description = source.readString();
+ boolean onHost = source.readInt() != 0;
+ String offHostName = source.readString();
+ String staticOffHostName = source.readString();
+ ArrayList<AidGroup> staticAidGroups = new ArrayList<AidGroup>();
+ int numStaticGroups = source.readInt();
+ if (numStaticGroups > 0) {
+ source.readTypedList(staticAidGroups, AidGroup.CREATOR);
+ }
+ ArrayList<AidGroup> dynamicAidGroups = new ArrayList<AidGroup>();
+ int numDynamicGroups = source.readInt();
+ if (numDynamicGroups > 0) {
+ source.readTypedList(dynamicAidGroups, AidGroup.CREATOR);
+ }
+ boolean requiresUnlock = source.readInt() != 0;
+ boolean requiresScreenOn = source.readInt() != 0;
+ int bannerResource = source.readInt();
+ int uid = source.readInt();
+ String settingsActivityName = source.readString();
+ boolean isSelected = source.readInt() != 0;
+ return new ApduServiceInfo(info, onHost, description, staticAidGroups,
+ dynamicAidGroups, requiresUnlock, requiresScreenOn, bannerResource, uid,
+ settingsActivityName, offHostName, staticOffHostName,
+ isSelected);
+ }
- @Override
- public ApduServiceInfo[] newArray(int size) {
- return new ApduServiceInfo[size];
- }
- };
+ @Override
+ public ApduServiceInfo[] newArray(int size) {
+ return new ApduServiceInfo[size];
+ }
+ };
/**
* Dump contents for debugging.
@@ -779,14 +806,16 @@
}
pw.println(" Static AID groups:");
for (AidGroup group : mStaticAidGroups.values()) {
- pw.println(" Category: " + group.getCategory());
+ pw.println(" Category: " + group.getCategory()
+ + "(selected: " + mOtherServiceSelectionState + ")");
for (String aid : group.getAids()) {
pw.println(" AID: " + aid);
}
}
pw.println(" Dynamic AID groups:");
for (AidGroup group : mDynamicAidGroups.values()) {
- pw.println(" Category: " + group.getCategory());
+ pw.println(" Category: " + group.getCategory()
+ + "(selected: " + mOtherServiceSelectionState + ")");
for (String aid : group.getAids()) {
pw.println(" AID: " + aid);
}
@@ -796,6 +825,22 @@
pw.println(" Requires Device ScreenOn: " + mRequiresDeviceScreenOn);
}
+
+ /**
+ * @hide
+ */
+ public void setOtherServiceState(boolean selected) {
+ mOtherServiceSelectionState = selected;
+ }
+
+
+ /**
+ * @hide
+ */
+ public boolean isSelectedOtherService() {
+ return mOtherServiceSelectionState;
+ }
+
/**
* Dump debugging info as ApduServiceInfoProto.
*
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index 32c2a1b..d048b59 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -16,17 +16,21 @@
package android.nfc.cardemulation;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.UserIdInt;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.nfc.Constants;
+import android.nfc.Flags;
import android.nfc.INfcCardEmulation;
import android.nfc.NfcAdapter;
import android.os.RemoteException;
@@ -877,9 +881,16 @@
}
/**
+ * Retrieves list of services registered of the provided category for the provided user.
+ *
+ * @param category Category string, one of {@link #CATEGORY_PAYMENT} or {@link #CATEGORY_OTHER}
+ * @param userId the user handle of the user whose information is being requested.
* @hide
*/
- public List<ApduServiceInfo> getServices(String category, int userId) {
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE)
+ @NonNull
+ public List<ApduServiceInfo> getServices(@NonNull String category, @UserIdInt int userId) {
try {
return sService.getServices(userId, category);
} catch (RemoteException e) {
@@ -936,6 +947,39 @@
return true;
}
+ /**
+ * Allows to set or unset preferred service (category other) to avoid AID Collision.
+ *
+ * @param service The ComponentName of the service
+ * @param status true to enable, false to disable
+ * @return set service for the category and true if service is already set return false.
+ *
+ * @hide
+ */
+ public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status) {
+ if (service == null) {
+ throw new NullPointerException("activity or service or category is null");
+ }
+ int userId = mContext.getUser().getIdentifier();
+
+ try {
+ return sService.setServiceEnabledForCategoryOther(userId, service, status);
+ } catch (RemoteException e) {
+ // Try one more time
+ recoverService();
+ if (sService == null) {
+ Log.e(TAG, "Failed to recover CardEmulationService.");
+ return false;
+ }
+ try {
+ return sService.setServiceEnabledForCategoryOther(userId, service, status);
+ } catch (RemoteException ee) {
+ Log.e(TAG, "Failed to reach CardEmulationService.");
+ return false;
+ }
+ }
+ }
+
void recoverService() {
NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext);
sService = adapter.getCardEmulationService();
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 218d4bb..3db1cb0 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -665,6 +665,32 @@
*/
public static final native void blockUntilThreadAvailable();
+
+ /**
+ * TODO (b/308179628): Move this to libbinder for non-Java usages.
+ */
+ private static IBinderCallback sBinderCallback = null;
+
+ /**
+ * Set callback function for unexpected binder transaction errors.
+ *
+ * @hide
+ */
+ public static final void setTransactionCallback(IBinderCallback callback) {
+ sBinderCallback = callback;
+ }
+
+ /**
+ * Execute the callback function if it's already set.
+ *
+ * @hide
+ */
+ public static final void transactionCallback(int pid, int code, int flags, int err) {
+ if (sBinderCallback != null) {
+ sBinderCallback.onTransactionError(pid, code, flags, err);
+ }
+ }
+
/**
* Default constructor just initializes the object.
*
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 509c3b8..a9b7257 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -17,6 +17,7 @@
package android.os;
import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -1227,6 +1228,7 @@
/**
* Vanilla Ice Cream.
*/
+ @FlaggedApi(Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM)
public static final int VANILLA_ICE_CREAM = CUR_DEVELOPMENT;
}
diff --git a/core/java/android/os/IBinderCallback.java b/core/java/android/os/IBinderCallback.java
new file mode 100644
index 0000000..e4be5b0
--- /dev/null
+++ b/core/java/android/os/IBinderCallback.java
@@ -0,0 +1,34 @@
+/*
+ * 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 android.os;
+
+/**
+ * Callback interface for binder transaction errors
+ *
+ * @hide
+ */
+public interface IBinderCallback {
+ /**
+ * Callback function for unexpected binder transaction errors.
+ *
+ * @param debugPid The binder transaction sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ void onTransactionError(int debugPid, int code, int flags, int err);
+}
diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java
index 87c4f33..9d8a71b 100644
--- a/core/java/android/os/MessageQueue.java
+++ b/core/java/android/os/MessageQueue.java
@@ -25,6 +25,8 @@
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
+import dalvik.annotation.optimization.CriticalNative;
+
import java.io.FileDescriptor;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -70,6 +72,7 @@
private native static void nativeDestroy(long ptr);
@UnsupportedAppUsage
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+ @CriticalNative
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index 86f03cd..940ddf2 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -1,6 +1,13 @@
package: "android.os"
flag {
+ name: "android_os_build_vanilla_ice_cream"
+ namespace: "build"
+ description: "Feature flag for adding the VANILLA_ICE_CREAM constant."
+ bug: "264658905"
+}
+
+flag {
name: "state_of_health_public"
namespace: "system_sw_battery"
description: "Feature flag for making state_of_health a public api."
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index e22207c..9d88af9 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -70,7 +70,6 @@
import android.os.Parcelable;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
@@ -3304,7 +3303,7 @@
checkPreconditions(sc);
if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
- "setCornerRadius", this, sc, "w=" + width + " h=" + height);
+ "setWindowCrop", this, sc, "w=" + width + " h=" + height);
}
nativeSetWindowCrop(mNativeObject, sc.mNativeObject, 0, 0, width, height);
return this;
diff --git a/core/java/com/android/internal/os/BinderfsStatsReader.java b/core/java/com/android/internal/os/BinderfsStatsReader.java
new file mode 100644
index 0000000..9cc4a35
--- /dev/null
+++ b/core/java/com/android/internal/os/BinderfsStatsReader.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal.os;
+
+import com.android.internal.util.ProcFileReader;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Reads and parses {@code binder_logs/stats} file in the {@code binderfs} filesystem.
+ * Reuse procFileReader as the contents are generated by Linux kernel in the same way.
+ *
+ * A typical example of binderfs stats log
+ *
+ * binder stats:
+ * BC_TRANSACTION: 378004
+ * BC_REPLY: 268352
+ * BC_FREE_BUFFER: 665854
+ * ...
+ * proc 12645
+ * context binder
+ * threads: 12
+ * requested threads: 0+5/15
+ * ready threads 0
+ * free async space 520192
+ * ...
+ */
+public class BinderfsStatsReader {
+ private final String mPath;
+
+ public BinderfsStatsReader() {
+ mPath = "/dev/binderfs/binder_logs/stats";
+ }
+
+ public BinderfsStatsReader(String path) {
+ mPath = path;
+ }
+
+ /**
+ * Read binderfs stats and call the consumer(pid, free) function for each valid process
+ *
+ * @param predicate Test if the pid is valid.
+ * @param biConsumer Callback function for each valid pid and its free async space
+ * @param consumer The error function to deal with exceptions
+ */
+ public void handleFreeAsyncSpace(Predicate<Integer> predicate,
+ BiConsumer<Integer, Integer> biConsumer, Consumer<Exception> consumer) {
+ try (ProcFileReader mReader = new ProcFileReader(new FileInputStream(mPath))) {
+ while (mReader.hasMoreData()) {
+ // find the next process
+ if (!mReader.nextString().equals("proc")) {
+ mReader.finishLine();
+ continue;
+ }
+
+ // read pid
+ int pid = mReader.nextInt();
+ mReader.finishLine();
+
+ // check if we have interest in this process
+ if (!predicate.test(pid)) {
+ continue;
+ }
+
+ // read free async space
+ mReader.finishLine(); // context binder
+ mReader.finishLine(); // threads:
+ mReader.finishLine(); // requested threads:
+ mReader.finishLine(); // ready threads
+ if (!mReader.nextString().equals("free")) {
+ mReader.finishLine();
+ continue;
+ }
+ if (!mReader.nextString().equals("async")) {
+ mReader.finishLine();
+ continue;
+ }
+ if (!mReader.nextString().equals("space")) {
+ mReader.finishLine();
+ continue;
+ }
+ int free = mReader.nextInt();
+ mReader.finishLine();
+ biConsumer.accept(pid, free);
+ }
+ } catch (IOException | NumberFormatException e) {
+ consumer.accept(e);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 3e9458d..b462c21 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -27,6 +27,7 @@
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN;
@@ -215,6 +216,12 @@
*/
public static final int ACTION_SMARTSPACE_DOORBELL = 22;
+ /**
+ * Time it takes to lazy-load the image of a {@link android.app.Notification.BigPictureStyle}
+ * notification.
+ */
+ public static final int ACTION_NOTIFICATION_BIG_PICTURE_LOADED = 23;
+
private static final int[] ACTIONS_ALL = {
ACTION_EXPAND_PANEL,
ACTION_TOGGLE_RECENTS,
@@ -239,6 +246,7 @@
ACTION_REQUEST_IME_SHOWN,
ACTION_REQUEST_IME_HIDDEN,
ACTION_SMARTSPACE_DOORBELL,
+ ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
};
/** @hide */
@@ -266,6 +274,7 @@
ACTION_REQUEST_IME_SHOWN,
ACTION_REQUEST_IME_HIDDEN,
ACTION_SMARTSPACE_DOORBELL,
+ ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Action {
@@ -296,6 +305,7 @@
UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN,
UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN,
UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL,
+ UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
};
private final Object mLock = new Object();
@@ -480,6 +490,8 @@
return "ACTION_REQUEST_IME_HIDDEN";
case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL:
return "ACTION_SMARTSPACE_DOORBELL";
+ case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED:
+ return "ACTION_NOTIFICATION_BIG_PICTURE_LOADED";
default:
throw new IllegalArgumentException("Invalid action");
}
diff --git a/core/jni/android_os_MessageQueue.cpp b/core/jni/android_os_MessageQueue.cpp
index 30d9ea1..9525605 100644
--- a/core/jni/android_os_MessageQueue.cpp
+++ b/core/jni/android_os_MessageQueue.cpp
@@ -225,7 +225,7 @@
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
-static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
+static void android_os_MessageQueue_nativeWake(jlong ptr) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->wake();
}
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index bfd80a9e..d2d5186 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -73,6 +73,7 @@
jclass mClass;
jmethodID mExecTransact;
jmethodID mGetInterfaceDescriptor;
+ jmethodID mTransactionCallback;
// Object state.
jfieldID mObject;
@@ -1173,6 +1174,8 @@
gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor",
"()Ljava/lang/String;");
+ gBinderOffsets.mTransactionCallback =
+ GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V");
gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
return RegisterMethodsOrDie(
@@ -1387,7 +1390,12 @@
if (err == NO_ERROR) {
return JNI_TRUE;
- } else if (err == UNKNOWN_TRANSACTION) {
+ }
+
+ env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(),
+ code, flags, err);
+
+ if (err == UNKNOWN_TRANSACTION) {
return JNI_FALSE;
}
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 56066b2..b12e147 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -1121,10 +1121,8 @@
}
}
// Fallback done
-
- fail_fn(CREATE_ERROR("Unable to find %s:%lld in %s", package_name.data(),
- ce_data_inode, parent_path.data()));
- return nullptr;
+ ALOGW("Unable to find %s:%lld in %s", package_name.data(), ce_data_inode, parent_path.data());
+ return "";
}
}
@@ -1145,11 +1143,19 @@
true /*call_fail_fn*/);
std::string ce_data_path = getAppDataDirName(mirrorCePath, package_name, ce_data_inode, fail_fn);
+ if (ce_data_path.empty()) {
+ ALOGE("Ignoring missing CE app data dir for %s\n", package_name.data());
+ return;
+ }
if (!createAndMountAppData(package_name, ce_data_path, mirrorCePath, actualCePath, fail_fn,
false /*call_fail_fn*/)) {
// CE might unlocks and the name is decrypted
// get the name and mount again
ce_data_path=getAppDataDirName(mirrorCePath, package_name, ce_data_inode, fail_fn);
+ if (ce_data_path.empty()) {
+ ALOGE("Ignoring missing CE app data dir for %s\n", package_name.data());
+ return;
+ }
mountAppData(package_name, ce_data_path, mirrorCePath, actualCePath, fail_fn);
}
}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 14f268a..7ad6e8d 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -16,7 +16,9 @@
package com.android.server.broadcastradio.aidl;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import android.app.compat.CompatChanges;
import android.hardware.broadcastradio.AmFmBandRange;
@@ -43,14 +45,16 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.stubbing.Answer;
import java.util.Map;
import java.util.Set;
public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase {
- private static final int U_APP_UID = 1001;
- private static final int T_APP_UID = 1002;
+ private static final int T_APP_UID = 1001;
+ private static final int U_APP_UID = 1002;
+ private static final int V_APP_UID = 1003;
private static final int FM_LOWER_LIMIT = 87_500;
private static final int FM_UPPER_LIMIT = 108_000;
@@ -133,10 +137,18 @@
@Before
public void setUp() {
- doReturn(true).when(() -> CompatChanges.isChangeEnabled(
- ConversionUtils.RADIO_U_VERSION_REQUIRED, U_APP_UID));
- doReturn(false).when(() -> CompatChanges.isChangeEnabled(
- ConversionUtils.RADIO_U_VERSION_REQUIRED, T_APP_UID));
+ doAnswer((Answer<Boolean>) invocationOnMock -> {
+ long changeId = invocationOnMock.getArgument(0);
+ int uid = invocationOnMock.getArgument(1);
+ if (uid == V_APP_UID) {
+ return changeId == ConversionUtils.RADIO_V_VERSION_REQUIRED
+ || changeId == ConversionUtils.RADIO_U_VERSION_REQUIRED;
+ } else if (uid == U_APP_UID) {
+ return changeId == ConversionUtils.RADIO_U_VERSION_REQUIRED;
+ }
+ return false;
+ }
+ ).when(() -> CompatChanges.isChangeEnabled(anyLong(), anyInt()));
}
@Test
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
new file mode 100644
index 0000000..e9f6450
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.internal.os;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.util.IntArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BinderfsStatsReaderTest {
+ private static final String BINDER_LOGS_STATS_HEADER = """
+ binder stats:
+ BC_TRANSACTION: 695756
+ BC_REPLY: 547779
+ BC_FREE_BUFFER: 1283223
+ BR_FAILED_REPLY: 4
+ BR_FROZEN_REPLY: 3
+ BR_ONEWAY_SPAM_SUSPECT: 1
+ proc: active 313 total 377
+ thread: active 3077 total 5227
+ """;
+ private static final String BINDER_LOGS_STATS_PROC1 = """
+ proc 14505
+ context binder
+ threads: 4
+ requested threads: 0+2/15
+ ready threads 0
+ free async space 520192
+ nodes: 9
+ refs: 29 s 29 w 29
+ buffers: 0
+ """;
+ private static final String BINDER_LOGS_STATS_PROC2 = """
+ proc 14461
+ context binder
+ threads: 8
+ requested threads: 0+2/15
+ ready threads 0
+ free async space 62
+ nodes: 30
+ refs: 51 s 51 w 51
+ buffers: 0
+ """;
+ private static final String BINDER_LOGS_STATS_PROC3 = """
+ proc 542
+ context binder
+ threads: 2
+ requested threads: 0+0/15
+ ready threads 0
+ free async space 519896
+ nodes: 1
+ refs: 2 s 3 w 2
+ buffers: 1
+ """;
+ private static final String BINDER_LOGS_STATS_PROC4 = """
+ proc 540
+ context binder
+ threads: 1
+ requested threads: 0+0/0
+ ready threads 1
+ free async space 44
+ nodes: 4
+ refs: 1 s 1 w 1
+ buffers: 0
+ """;
+ private File mStatsDirectory;
+ private int mFreezerBinderAsyncThreshold;
+ private IntArray mValidPids; // The pool of valid pids
+ private IntArray mStatsPids; // The pids read from binderfs stats that are also valid
+ private IntArray mStatsFree; // The free async space of the above pids
+ private boolean mHasError;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getContext();
+ mStatsDirectory = context.getDir("binder_logs", Context.MODE_PRIVATE);
+ mFreezerBinderAsyncThreshold = 1024;
+ mValidPids = IntArray.fromArray(new int[]{14505, 14461, 542, 540}, 4);
+ mStatsPids = new IntArray();
+ mStatsFree = new IntArray();
+ mHasError = false;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ FileUtils.deleteContents(mStatsDirectory);
+ }
+
+ @Test
+ public void testNoneProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER);
+ assertFalse(mHasError);
+ assertEquals(0, mStatsPids.size());
+ assertEquals(0, mStatsFree.size());
+ }
+
+ @Test
+ public void testOneProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1);
+ assertFalse(mHasError);
+ assertEquals(0, mStatsPids.size());
+ assertEquals(0, mStatsFree.size());
+ }
+
+ @Test
+ public void testTwoProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2);
+ assertFalse(mHasError);
+ assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+ assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+ }
+
+ @Test
+ public void testThreeProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3);
+ assertFalse(mHasError);
+ assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+ assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+ }
+
+ @Test
+ public void testFourProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+ assertFalse(mHasError);
+ assertArrayEquals(mStatsPids.toArray(), new int[]{14461, 540});
+ assertArrayEquals(mStatsFree.toArray(), new int[]{62, 44});
+ }
+
+ @Test
+ public void testInvalidProc() throws Exception {
+ mValidPids = new IntArray();
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+ assertFalse(mHasError);
+ assertEquals(0, mStatsPids.size());
+ assertEquals(0, mStatsFree.size());
+ }
+
+ private void runHandleBlockingFileLocks(String fileContents) throws Exception {
+ File tempFile = File.createTempFile("stats", null, mStatsDirectory);
+ Files.write(tempFile.toPath(), fileContents.getBytes());
+ new BinderfsStatsReader(tempFile.toString()).handleFreeAsyncSpace(
+ // Check if the current process is a valid one
+ mValidPids::contains,
+
+ // Check if the current process is running out of async binder space
+ (pid, free) -> {
+ if (free < mFreezerBinderAsyncThreshold) {
+ mStatsPids.add(pid);
+ mStatsFree.add(free);
+ }
+ },
+
+ // Log the error if binderfs stats can't be accesses or correctly parsed
+ exception -> {
+ mHasError = true;
+ });
+ Files.delete(tempFile.toPath());
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 7743ad5..76f0b67 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1280,9 +1280,9 @@
// Check whether the Intent should be embedded in the known Task.
final TaskContainer taskContainer = mTaskContainers.valueAt(0);
if (taskContainer.isInPictureInPicture()
- || taskContainer.getTopNonFinishingActivity() == null) {
+ || taskContainer.getTopNonFinishingActivity(false /* includeOverlay */) == null) {
// We don't embed activity when it is in PIP, or if we can't find any other owner
- // activity in the Task.
+ // activity in non-overlay container in the Task.
return null;
}
@@ -1431,7 +1431,7 @@
} else {
final TaskContainer taskContainer = getTaskContainer(taskId);
activityInTask = taskContainer != null
- ? taskContainer.getTopNonFinishingActivity()
+ ? taskContainer.getTopNonFinishingActivity(true /* includeOverlay */)
: null;
}
if (activityInTask == null) {
@@ -1763,10 +1763,6 @@
return;
}
- if (container.isFinished()) {
- return;
- }
-
if (container.isOverlay()) {
updateOverlayContainer(wct, container);
return;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index eeb3ccf..028e75f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -235,9 +235,13 @@
}
@Nullable
- Activity getTopNonFinishingActivity() {
+ Activity getTopNonFinishingActivity(boolean includeOverlay) {
for (int i = mContainers.size() - 1; i >= 0; i--) {
- final Activity activity = mContainers.get(i).getTopNonFinishingActivity();
+ final TaskFragmentContainer container = mContainers.get(i);
+ if (!includeOverlay && container.isOverlay()) {
+ continue;
+ }
+ final Activity activity = container.getTopNonFinishingActivity();
if (activity != null) {
return activity;
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index e74d5fb..50cfd94 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -415,6 +415,17 @@
}
@Test
+ public void testGetTopNonFinishingActivityWithOverlay() {
+ createTestOverlayContainer(TASK_ID, "test1");
+ final Activity activity = createMockActivity();
+ final TaskFragmentContainer container = createMockTaskFragmentContainer(activity);
+ final TaskContainer task = container.getTaskContainer();
+
+ assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)).isEqualTo(mActivity);
+ assertThat(task.getTopNonFinishingActivity(false /* includeOverlay */)).isEqualTo(activity);
+ }
+
+ @Test
public void testUpdateContainer_dontInvokeUpdateOverlayForNonOverlayContainer() {
TaskFragmentContainer taskFragmentContainer = createMockTaskFragmentContainer(mActivity);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index e3f5169..e56c8ab 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -151,21 +151,24 @@
@Test
public void testGetTopNonFinishingActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- assertNull(taskContainer.getTopNonFinishingActivity());
+ assertNull(taskContainer.getTopNonFinishingActivity(true /* includeOverlay */));
final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class);
taskContainer.addTaskFragmentContainer(tf0);
final Activity activity0 = mock(Activity.class);
doReturn(activity0).when(tf0).getTopNonFinishingActivity();
- assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
+ assertEquals(activity0, taskContainer.getTopNonFinishingActivity(
+ true /* includeOverlay */));
final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class);
taskContainer.addTaskFragmentContainer(tf1);
- assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
+ assertEquals(activity0, taskContainer.getTopNonFinishingActivity(
+ true /* includeOverlay */));
final Activity activity1 = mock(Activity.class);
doReturn(activity1).when(tf1).getTopNonFinishingActivity();
- assertEquals(activity1, taskContainer.getTopNonFinishingActivity());
+ assertEquals(activity1, taskContainer.getTopNonFinishingActivity(
+ true /* includeOverlay */));
}
@Test
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 51c71b1..0e59e9a 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -15,10 +15,11 @@
}
flag {
- name: "desktop_windowing"
+ name: "enable_desktop_windowing"
namespace: "multitasking"
description: "Enables desktop windowing"
bug: "304778354"
+ is_fixed_read_only: true
}
flag {
diff --git a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
index c82a70c..5c58158 100644
--- a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
+++ b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
@@ -48,7 +48,7 @@
optional int32 handler = 3;
optional int64 merge_time_ns = 4;
optional int64 merge_request_time_ns = 5;
- optional int32 merged_into = 6;
+ optional int32 merge_target = 6;
optional int64 abort_time_ns = 7;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 7783113..dc82fc1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -18,11 +18,15 @@
import android.os.SystemProperties;
+import com.android.wm.shell.Flags;
+
/**
* Constants for desktop mode feature
*/
public class DesktopModeStatus {
+ private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing();
+
/**
* Flag to indicate whether desktop mode proto is available on the device
*/
@@ -54,6 +58,12 @@
* Return {@code true} is desktop windowing proto 2 is enabled
*/
public static boolean isEnabled() {
+ // Check for aconfig flag first
+ if (ENABLE_DESKTOP_WINDOWING) {
+ return true;
+ }
+ // Fall back to sysprop flag
+ // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding
return IS_PROTO2_ENABLED;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
index e27e4f9..5919aad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
@@ -129,13 +129,12 @@
* Adds an entry in the trace to log that a request to merge a transition was made.
*
* @param mergeRequestedTransitionId The id of the transition we are requesting to be merged.
- * @param playingTransitionId The id of the transition we was to merge the transition into.
*/
public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) {
com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
proto.id = mergeRequestedTransitionId;
proto.mergeRequestTimeNs = SystemClock.elapsedRealtimeNanos();
- proto.mergedInto = playingTransitionId;
+ proto.mergeTarget = playingTransitionId;
mTraceBuffer.add(proto);
}
@@ -150,7 +149,7 @@
com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
proto.id = mergedTransitionId;
proto.mergeTimeNs = SystemClock.elapsedRealtimeNanos();
- proto.mergedInto = playingTransitionId;
+ proto.mergeTarget = playingTransitionId;
mTraceBuffer.add(proto);
}
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index e986c38..78a6479 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -34,3 +34,10 @@
description: "Clip z-above surfaceviews to global clip rect"
bug: "298621623"
}
+
+flag {
+ name: "requested_formats_v"
+ namespace: "core_graphics"
+ description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK"
+ bug: "292545615"
+}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9ad5c3e..eea6357 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -21,9 +21,11 @@
import static android.content.Context.DEVICE_ID_DEFAULT;
import static com.android.media.audio.flags.Flags.autoPublicVolumeApiHardening;
+import static com.android.media.audio.flags.Flags.FLAG_FOCUS_FREEZE_TEST_API;
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -4751,6 +4753,7 @@
* @return the list of UIDs, can be empty when no app is being ducked.
*/
@TestApi
+ @FlaggedApi(FLAG_FOCUS_FREEZE_TEST_API)
@RequiresPermission("android.permission.QUERY_AUDIO_STATE")
public @NonNull List<Integer> getFocusDuckedUidsForTest() {
try {
@@ -4766,6 +4769,7 @@
* @return the fade out duration in ms
*/
@TestApi
+ @FlaggedApi(FLAG_FOCUS_FREEZE_TEST_API)
@RequiresPermission("android.permission.QUERY_AUDIO_STATE")
public long getFocusFadeOutDurationForTest() {
try {
@@ -4782,6 +4786,7 @@
* @return the time gap after a fade-out completion on focus loss, and fade-in start in ms.
*/
@TestApi
+ @FlaggedApi(FLAG_FOCUS_FREEZE_TEST_API)
@RequiresPermission("android.permission.QUERY_AUDIO_STATE")
public long getFocusUnmuteDelayAfterFadeOutForTest() {
try {
@@ -4808,6 +4813,7 @@
* in a proper state with a predictable behavior for audio focus management.
*/
@TestApi
+ @FlaggedApi(FLAG_FOCUS_FREEZE_TEST_API)
@RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
public boolean enterAudioFocusFreezeForTest(@NonNull List<Integer> exemptedUids) {
Objects.requireNonNull(exemptedUids);
@@ -4826,6 +4832,7 @@
* such as the freeze already having ended, or not started.
*/
@TestApi
+ @FlaggedApi(FLAG_FOCUS_FREEZE_TEST_API)
@RequiresPermission("Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED")
public boolean exitAudioFocusFreezeForTest() {
try {
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index cccf6f1..937151b 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -880,6 +880,7 @@
.append(", volumeHandling=").append(getVolumeHandling())
.append(", volumeMax=").append(getVolumeMax())
.append(", volume=").append(getVolume())
+ .append(", address=").append(getAddress())
.append(", deduplicationIds=").append(String.join(",", getDeduplicationIds()))
.append(", providerId=").append(getProviderId())
.append(", isVisibilityRestricted=").append(mIsVisibilityRestricted)
diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
index 87bfc81..ecd500e 100644
--- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
+++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java
@@ -21,12 +21,13 @@
import android.view.View;
import androidx.preference.PreferenceViewHolder;
-import androidx.preference.SwitchPreference;
+import androidx.preference.SwitchPreferenceCompat;
+
import com.android.settingslib.widget.preference.app.R;
/**
* The SwitchPreference for the pages need to show apps icon.
*/
-public class AppSwitchPreference extends SwitchPreference {
+public class AppSwitchPreference extends SwitchPreferenceCompat {
public AppSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
@@ -52,7 +53,7 @@
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- final View switchView = holder.findViewById(android.R.id.switch_widget);
+ final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
if (switchView != null) {
final View rootView = switchView.getRootView();
rootView.setFilterTouchesWhenObscured(true);
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index 009407a..eaeda3c 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -38,7 +38,6 @@
static_libs: [
"androidx.compose.runtime_runtime",
"SpaPrivilegedLib",
- "android.content.pm.flags-aconfig-java",
],
kotlincflags: ["-Xjvm-default=all"],
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index 92fd0cd..95e678f 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -35,7 +35,7 @@
fun ApplicationInfo.hasGrantPermission(permission: String): Boolean
suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String>
- fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo?
+ fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo?
}
object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl)
@@ -72,14 +72,16 @@
?: false
override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
- val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+ val packageInfo =
+ getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
return packageInfo?.requestedPermissions?.let {
permission in it
} ?: false
}
override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
- val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+ val packageInfo =
+ getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false
return index >= 0 &&
checkNotNull(packageInfo.requestedPermissionsFlags)[index]
@@ -91,8 +93,8 @@
iPackageManager.isPackageAvailable(it, userId)
}.toSet()
- override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
- packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
+ override fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo? =
+ packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags, userId)
private fun Int.hasFlag(flag: Int) = (this and flag) > 0
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
index 7fbd35b..0a2d9fc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/PrimarySwitchPreference.java
@@ -18,8 +18,11 @@
import android.content.Context;
import android.util.AttributeSet;
+import android.view.Gravity;
import android.view.MotionEvent;
+import android.view.View;
import android.widget.CompoundButton;
+import android.widget.LinearLayout;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
@@ -59,13 +62,17 @@
@Override
protected int getSecondTargetResId() {
- return R.layout.preference_widget_primary_switch;
+ return androidx.preference.R.layout.preference_widget_switch_compat;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
- mSwitch = (CompoundButton) holder.findViewById(R.id.switchWidget);
+ final View widgetFrame = holder.findViewById(android.R.id.widget_frame);
+ if (widgetFrame instanceof LinearLayout linearLayout) {
+ linearLayout.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
+ }
+ mSwitch = (CompoundButton) holder.findViewById(androidx.preference.R.id.switchWidget);
if (mSwitch != null) {
mSwitch.setOnClickListener(v -> {
if (mSwitch != null && !mSwitch.isEnabled()) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index c67df71..b0832e3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -16,6 +16,7 @@
package com.android.settingslib.bluetooth;
+import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -39,6 +40,7 @@
import android.util.LruCache;
import android.util.Pair;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -52,8 +54,12 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
import java.util.stream.Stream;
/**
@@ -101,6 +107,8 @@
private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>();
+ private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>();
+
/**
* Last time a bt profile auto-connect was attempted.
* If an ACTION_UUID intent comes in within
@@ -992,18 +1000,39 @@
return new ArrayList<>(mRemovedProfiles);
}
+ /**
+ * @deprecated Use {@link #registerCallback(Executor, Callback)}.
+ */
+ @Deprecated
public void registerCallback(Callback callback) {
mCallbacks.add(callback);
}
+ /**
+ * Registers a {@link Callback} that will be invoked when the bluetooth device attribute is
+ * changed.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link Callback}
+ */
+ public void registerCallback(
+ @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ mCallbackExecutorMap.put(callback, executor);
+ }
+
public void unregisterCallback(Callback callback) {
mCallbacks.remove(callback);
+ mCallbackExecutorMap.remove(callback);
}
void dispatchAttributesChanged() {
for (Callback callback : mCallbacks) {
callback.onDeviceAttributesChanged();
}
+ mCallbackExecutorMap.forEach((callback, executor) ->
+ executor.execute(callback::onDeviceAttributesChanged));
}
@Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java
deleted file mode 100644
index 6b855c0..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.qrcode;
-
-import android.graphics.Bitmap;
-import android.graphics.Color;
-
-import com.google.zxing.BarcodeFormat;
-import com.google.zxing.EncodeHintType;
-import com.google.zxing.MultiFormatWriter;
-import com.google.zxing.WriterException;
-import com.google.zxing.common.BitMatrix;
-
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-
-public final class QrCodeGenerator {
- private static final int DEFAULT_MARGIN = -1;
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size)
- throws WriterException, IllegalArgumentException {
- return encodeQrCode(contents, size, DEFAULT_MARGIN, /*invert=*/false);
- }
-
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @param margin The margin around the actual barcode
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size, int margin)
- throws WriterException, IllegalArgumentException {
- return encodeQrCode(contents, size, margin, /*invert=*/false);
- }
-
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size, boolean invert)
- throws WriterException, IllegalArgumentException {
- return encodeQrCode(contents, size, DEFAULT_MARGIN, /*invert=*/invert);
- }
-
- /**
- * Generates a barcode image with {@code contents}.
- *
- * @param contents The contents to encode in the barcode
- * @param size The preferred image size in pixels
- * @param margin The margin around the actual barcode
- * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
- * @return Barcode bitmap
- */
- public static Bitmap encodeQrCode(String contents, int size, int margin, boolean invert)
- throws WriterException, IllegalArgumentException {
- final Map<EncodeHintType, Object> hints = new HashMap<>();
- if (!isIso88591(contents)) {
- hints.put(EncodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
- }
- if (margin != DEFAULT_MARGIN) {
- hints.put(EncodeHintType.MARGIN, margin);
- }
-
- final BitMatrix qrBits = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE,
- size, size, hints);
- final Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565);
- int setColor = invert ? Color.WHITE : Color.BLACK;
- int unsetColor = invert ? Color.BLACK : Color.WHITE;
- for (int x = 0; x < size; x++) {
- for (int y = 0; y < size; y++) {
- bitmap.setPixel(x, y, qrBits.get(x, y) ? setColor : unsetColor);
- }
- }
- return bitmap;
- }
-
- private static boolean isIso88591(String contents) {
- CharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder();
- return encoder.canEncode(contents);
- }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt
new file mode 100644
index 0000000..7b67ec6
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeGenerator.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.settingslib.qrcode
+
+import android.annotation.ColorInt
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.EncodeHintType
+import com.google.zxing.MultiFormatWriter
+import com.google.zxing.WriterException
+import java.nio.charset.StandardCharsets
+import java.util.EnumMap
+
+object QrCodeGenerator {
+ /**
+ * Generates a barcode image with [contents].
+ *
+ * @param contents The contents to encode in the barcode
+ * @param size The preferred image size in pixels
+ * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
+ * @return Barcode bitmap
+ */
+ @JvmStatic
+ @Throws(WriterException::class, java.lang.IllegalArgumentException::class)
+ fun encodeQrCode(contents: String, size: Int, invert: Boolean): Bitmap =
+ encodeQrCode(contents, size, DEFAULT_MARGIN, invert)
+
+ private const val DEFAULT_MARGIN = -1
+
+ /**
+ * Generates a barcode image with [contents].
+ *
+ * @param contents The contents to encode in the barcode
+ * @param size The preferred image size in pixels
+ * @param margin The margin around the actual barcode
+ * @param invert Whether to invert the black/white pixels (e.g. for dark mode)
+ * @return Barcode bitmap
+ */
+ @JvmOverloads
+ @JvmStatic
+ @Throws(WriterException::class, IllegalArgumentException::class)
+ fun encodeQrCode(
+ contents: String,
+ size: Int,
+ margin: Int = DEFAULT_MARGIN,
+ invert: Boolean = false,
+ ): Bitmap {
+ val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
+ if (!isIso88591(contents)) {
+ hints[EncodeHintType.CHARACTER_SET] = StandardCharsets.UTF_8.name()
+ }
+ if (margin != DEFAULT_MARGIN) {
+ hints[EncodeHintType.MARGIN] = margin
+ }
+ val qrBits = MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, size, size, hints)
+ @ColorInt val setColor = if (invert) Color.WHITE else Color.BLACK
+ @ColorInt val unsetColor = if (invert) Color.BLACK else Color.WHITE
+ @ColorInt val pixels = IntArray(size * size)
+ for (x in 0 until size) {
+ for (y in 0 until size) {
+ pixels[x * size + y] = if (qrBits[x, y]) setColor else unsetColor
+ }
+ }
+ return Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565).apply {
+ setPixels(pixels, 0, size, 0, 0, size, size)
+ }
+ }
+
+ private fun isIso88591(contents: String): Boolean =
+ StandardCharsets.ISO_8859_1.newEncoder().canEncode(contents)
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
index debfa49..851a581 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/PrimarySwitchPreferenceTest.java
@@ -57,12 +57,14 @@
com.android.settingslib.widget.preference.twotarget.R.layout.preference_two_target,
null));
mWidgetView = mHolder.itemView.findViewById(android.R.id.widget_frame);
- inflater.inflate(R.layout.preference_widget_primary_switch, mWidgetView, true);
+ inflater.inflate(androidx.preference.R.layout.preference_widget_switch_compat, mWidgetView,
+ true);
}
@Test
public void setChecked_shouldUpdateButtonCheckedState() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
mPreference.setChecked(true);
@@ -74,7 +76,8 @@
@Test
public void setSwitchEnabled_shouldUpdateButtonEnabledState() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
mPreference.setSwitchEnabled(true);
@@ -86,7 +89,8 @@
@Test
public void setSwitchEnabled_shouldUpdateButtonEnabledState_beforeViewBound() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.setSwitchEnabled(false);
mPreference.onBindViewHolder(mHolder);
@@ -97,7 +101,8 @@
public void clickWidgetView_shouldToggleButton() {
assertThat(mWidgetView).isNotNull();
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
toggle.performClick();
@@ -111,7 +116,8 @@
public void clickWidgetView_shouldNotToggleButtonIfDisabled() {
assertThat(mWidgetView).isNotNull();
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
mPreference.onBindViewHolder(mHolder);
toggle.setEnabled(false);
@@ -122,7 +128,8 @@
@Test
public void clickWidgetView_shouldNotifyPreferenceChanged() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
final OnPreferenceChangeListener listener = mock(OnPreferenceChangeListener.class);
mPreference.setOnPreferenceChangeListener(listener);
@@ -139,7 +146,8 @@
@Test
public void setDisabledByAdmin_hasEnforcedAdmin_shouldDisableButton() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
toggle.setEnabled(true);
mPreference.onBindViewHolder(mHolder);
@@ -149,7 +157,8 @@
@Test
public void setDisabledByAdmin_noEnforcedAdmin_shouldEnableButton() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
toggle.setEnabled(false);
mPreference.onBindViewHolder(mHolder);
@@ -159,7 +168,8 @@
@Test
public void onBindViewHolder_toggleButtonShouldHaveContentDescription() {
- final CompoundButton toggle = (CompoundButton) mHolder.findViewById(R.id.switchWidget);
+ final CompoundButton toggle =
+ (CompoundButton) mHolder.findViewById(androidx.preference.R.id.switchWidget);
final String label = "TestButton";
mPreference.setTitle(label);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 5acc1ca..8c8a71c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -990,7 +990,6 @@
IntentFilter userFilter = new IntentFilter();
userFilter.addAction(Intent.ACTION_USER_ADDED);
userFilter.addAction(Intent.ACTION_USER_REMOVED);
- userFilter.addAction(Intent.ACTION_USER_STOPPED);
getContext().registerReceiver(new BroadcastReceiver() {
@Override
@@ -1015,11 +1014,6 @@
mSettingsRegistry.removeUserStateLocked(userId, true);
}
}
- case Intent.ACTION_USER_STOPPED -> {
- synchronized (mLock) {
- mSettingsRegistry.removeUserStateLocked(userId, false);
- }
- }
}
}
}, userFilter);
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 08ecf09b..25ac486 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -3,10 +3,10 @@
# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
flag {
- name: "floating_menu_overlaps_nav_bars_flag"
+ name: "floating_menu_animated_tuck"
namespace: "accessibility"
- description: "Adjusts bounds to allow the floating menu to render on top of navigation bars."
- bug: "283768342"
+ description: "Sets up animations for tucking/untucking and adjusts clipbounds."
+ bug: "24592044"
}
flag {
@@ -14,4 +14,11 @@
namespace: "accessibility"
description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
bug: "281150010"
+}
+
+flag {
+ name: "floating_menu_overlaps_nav_bars_flag"
+ namespace: "accessibility"
+ description: "Adjusts bounds to allow the floating menu to render on top of navigation bars."
+ bug: "283768342"
}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
new file mode 100644
index 0000000..82d4239
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+
+interface EdgeDetector {
+ /**
+ * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given
+ * [density] and [orientation].
+ */
+ fun edge(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): Edge?
+}
+
+val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp)
+
+/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */
+class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector {
+ override fun edge(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): Edge? {
+ val axisSize: Int
+ val axisPosition: Int
+ val topOrLeft: Edge
+ val bottomOrRight: Edge
+ when (orientation) {
+ Orientation.Horizontal -> {
+ axisSize = layoutSize.width
+ axisPosition = position.x
+ topOrLeft = Edge.Left
+ bottomOrRight = Edge.Right
+ }
+ Orientation.Vertical -> {
+ axisSize = layoutSize.height
+ axisPosition = position.y
+ topOrLeft = Edge.Top
+ bottomOrRight = Edge.Bottom
+ }
+ }
+
+ val sizePx = with(density) { size.toPx() }
+ return when {
+ axisPosition <= sizePx -> topOrLeft
+ axisPosition >= axisSize - sizePx -> bottomOrRight
+ else -> null
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index d005413..ae7d8f5 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,7 +2,6 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import kotlinx.coroutines.CoroutineScope
interface GestureHandler {
val draggable: DraggableHandler
@@ -10,9 +9,9 @@
}
interface DraggableHandler {
- suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset)
+ fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
fun onDelta(pixels: Float)
- suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float)
+ fun onDragStopped(velocity: Float)
}
interface NestedScrollHandler {
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
new file mode 100644
index 0000000..97d3fff
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
+import androidx.compose.foundation.gestures.horizontalDrag
+import androidx.compose.foundation.gestures.verticalDrag
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.input.pointer.util.addPointerInputChange
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Make an element draggable in the given [orientation].
+ *
+ * The main difference with [multiPointerDraggable] and
+ * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
+ * of pointers that are down when the drag is started. If you don't need this information, you
+ * should use `draggable` instead.
+ *
+ * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
+ * pointer, then we count the number of distinct pointers that are down right before calling
+ * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
+ * dragged) and a second pointer is down and dragged. This is an implementation detail that might
+ * change in the future.
+ */
+// TODO(b/291055080): Migrate to the Modifier.Node API.
+@Composable
+internal fun Modifier.multiPointerDraggable(
+ orientation: Orientation,
+ enabled: Boolean,
+ startDragImmediately: Boolean,
+ onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
+ onDragDelta: (Float) -> Unit,
+ onDragStopped: (velocity: Float) -> Unit,
+): Modifier {
+ val onDragStarted by rememberUpdatedState(onDragStarted)
+ val onDragStopped by rememberUpdatedState(onDragStopped)
+ val onDragDelta by rememberUpdatedState(onDragDelta)
+ val startDragImmediately by rememberUpdatedState(startDragImmediately)
+
+ val velocityTracker = remember { VelocityTracker() }
+ val maxFlingVelocity =
+ LocalViewConfiguration.current.maximumFlingVelocity.let { max ->
+ val maxF = max.toFloat()
+ Velocity(maxF, maxF)
+ }
+
+ return this.pointerInput(enabled, orientation, maxFlingVelocity) {
+ if (!enabled) {
+ return@pointerInput
+ }
+
+ val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
+ velocityTracker.resetTracking()
+ onDragStarted(startedPosition, pointersDown)
+ }
+
+ val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
+
+ val onDragEnd: () -> Unit = {
+ val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
+ onDragStopped(
+ when (orientation) {
+ Orientation.Horizontal -> velocity.x
+ Orientation.Vertical -> velocity.y
+ }
+ )
+ }
+
+ val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount ->
+ velocityTracker.addPointerInputChange(change)
+ onDragDelta(amount)
+ }
+
+ detectDragGestures(
+ orientation = orientation,
+ startDragImmediately = { startDragImmediately },
+ onDragStart = onDragStart,
+ onDragEnd = onDragEnd,
+ onDragCancel = onDragCancel,
+ onDrag = onDrag,
+ )
+ }
+}
+
+/**
+ * Detect drag gestures in the given [orientation].
+ *
+ * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
+ * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for:
+ * 1) starting the gesture immediately without requiring a drag >= touch slope;
+ * 2) passing the number of pointers down to [onDragStart].
+ */
+private suspend fun PointerInputScope.detectDragGestures(
+ orientation: Orientation,
+ startDragImmediately: () -> Boolean,
+ onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit,
+ onDragEnd: () -> Unit,
+ onDragCancel: () -> Unit,
+ onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
+) {
+ awaitEachGesture {
+ val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+ var overSlop = 0f
+ val drag =
+ if (startDragImmediately()) {
+ initialDown.consume()
+ initialDown
+ } else {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ val onSlopReached = { change: PointerInputChange, over: Float ->
+ change.consume()
+ overSlop = over
+ }
+
+ // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
+ // it is public.
+ when (orientation) {
+ Orientation.Horizontal ->
+ awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
+ Orientation.Vertical ->
+ awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
+ }
+ }
+
+ if (drag != null) {
+ // Count the number of pressed pointers.
+ val pressed = mutableSetOf<PointerId>()
+ currentEvent.changes.fastForEach { change ->
+ if (change.pressed) {
+ pressed.add(change.id)
+ }
+ }
+
+ onDragStart(drag.position, pressed.size)
+ onDrag(drag, overSlop)
+
+ val successful =
+ when (orientation) {
+ Orientation.Horizontal ->
+ horizontalDrag(drag.id) {
+ onDrag(it, it.positionChange().x)
+ it.consume()
+ }
+ Orientation.Vertical ->
+ verticalDrag(drag.id) {
+ onDrag(it, it.positionChange().y)
+ it.consume()
+ }
+ }
+
+ if (successful) {
+ onDragEnd()
+ } else {
+ onDragCancel()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 9c799b28..3fd6828 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -16,7 +16,6 @@
package com.android.compose.animation.scene
-import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -101,19 +100,3 @@
MovableElement(layoutImpl, scene, key, modifier, content)
}
}
-
-/** The destination scene when swiping up or left from [upOrLeft]. */
-internal fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
- return when (orientation) {
- Orientation.Vertical -> userActions[Swipe.Up]
- Orientation.Horizontal -> userActions[Swipe.Left]
- }
-}
-
-/** The destination scene when swiping down or right from [downOrRight]. */
-internal fun Scene.downOrRight(orientation: Orientation): SceneKey? {
- return when (orientation) {
- Orientation.Vertical -> userActions[Swipe.Down]
- Orientation.Horizontal -> userActions[Swipe.Right]
- }
-}
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 74e66d2..1f38e70 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
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
@@ -37,6 +38,7 @@
* instance by triggering back navigation or by swiping to a new scene.
* @param transitions the definition of the transitions used to animate a change of scene.
* @param state the observable state of this layout.
+ * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
* @param scenes the configuration of the different scenes of this layout.
*/
@Composable
@@ -46,6 +48,7 @@
transitions: SceneTransitions,
modifier: Modifier = Modifier,
state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+ edgeDetector: EdgeDetector = DefaultEdgeDetector,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
@@ -56,15 +59,17 @@
transitions,
state,
density,
+ edgeDetector,
)
}
layoutImpl.onChangeScene = onChangeScene
layoutImpl.transitions = transitions
layoutImpl.density = density
+ layoutImpl.edgeDetector = edgeDetector
+
layoutImpl.setScenes(scenes)
layoutImpl.setCurrentScene(currentScene)
-
layoutImpl.Content(modifier)
}
@@ -191,9 +196,9 @@
}
}
-enum class SwipeDirection {
- Up,
- Down,
- Left,
- Right,
+enum class SwipeDirection(val orientation: Orientation) {
+ Up(Orientation.Vertical),
+ Down(Orientation.Vertical),
+ Left(Orientation.Horizontal),
+ Right(Orientation.Horizontal),
}
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 a40b299..a803a47 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
@@ -47,6 +47,7 @@
transitions: SceneTransitions,
internal val state: SceneTransitionLayoutState,
density: Density,
+ edgeDetector: EdgeDetector,
) {
internal val scenes = SnapshotStateMap<SceneKey, Scene>()
internal val elements = SnapshotStateMap<ElementKey, Element>()
@@ -57,6 +58,7 @@
internal var onChangeScene by mutableStateOf(onChangeScene)
internal var transitions by mutableStateOf(transitions)
internal var density: Density by mutableStateOf(density)
+ internal var edgeDetector by mutableStateOf(edgeDetector)
/**
* The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 2dc53ab..ee1f133 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -22,8 +22,6 @@
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -37,6 +35,7 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
@@ -55,7 +54,7 @@
/** Whether swipe should be enabled in the given [orientation]. */
fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
- upOrLeft(orientation) != null || downOrRight(orientation) != null
+ userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
val currentScene = gestureHandler.currentScene
val canSwipe = currentScene.shouldEnableSwipes(orientation)
@@ -68,8 +67,7 @@
)
return nestedScroll(connection = gestureHandler.nestedScroll.connection)
- .draggable(
- state = rememberDraggableState(onDelta = gestureHandler.draggable::onDelta),
+ .multiPointerDraggable(
orientation = orientation,
enabled = gestureHandler.isDrivingTransition || canSwipe,
// Immediately start the drag if this our [transition] is currently animating to a scene
@@ -80,6 +78,7 @@
gestureHandler.isAnimatingOffset &&
!canOppositeSwipe,
onDragStarted = gestureHandler.draggable::onDragStarted,
+ onDragDelta = gestureHandler.draggable::onDelta,
onDragStopped = gestureHandler.draggable::onDragStopped,
)
}
@@ -159,7 +158,7 @@
internal var gestureWithPriority: Any? = null
- internal fun onDragStarted() {
+ internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
if (isDrivingTransition) {
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
@@ -199,6 +198,48 @@
Orientation.Vertical -> layoutImpl.size.height
}.toFloat()
+ val fromEdge =
+ startedPosition?.let { position ->
+ layoutImpl.edgeDetector.edge(
+ layoutImpl.size,
+ position.round(),
+ layoutImpl.density,
+ orientation,
+ )
+ }
+
+ swipeTransition.actionUpOrLeft =
+ Swipe(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal -> SwipeDirection.Left
+ Orientation.Vertical -> SwipeDirection.Up
+ },
+ pointerCount = pointersDown,
+ fromEdge = fromEdge,
+ )
+
+ swipeTransition.actionDownOrRight =
+ Swipe(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal -> SwipeDirection.Right
+ Orientation.Vertical -> SwipeDirection.Down
+ },
+ pointerCount = pointersDown,
+ fromEdge = fromEdge,
+ )
+
+ if (fromEdge == null) {
+ swipeTransition.actionUpOrLeftNoEdge = null
+ swipeTransition.actionDownOrRightNoEdge = null
+ } else {
+ swipeTransition.actionUpOrLeftNoEdge =
+ (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
+ swipeTransition.actionDownOrRightNoEdge =
+ (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
+ }
+
if (swipeTransition.absoluteDistance > 0f) {
transitionState = swipeTransition
}
@@ -246,11 +287,11 @@
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
val absoluteDistance = swipeTransition.absoluteDistance
- if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
+ if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
swipeTransition.dragOffset += absoluteDistance
swipeTransition._fromScene = toScene
} else if (
- offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key
+ offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
) {
swipeTransition.dragOffset -= absoluteDistance
swipeTransition._fromScene = toScene
@@ -272,8 +313,8 @@
Orientation.Vertical -> layoutImpl.size.height
}.toFloat()
- val upOrLeft = upOrLeft(orientation)
- val downOrRight = downOrRight(orientation)
+ val upOrLeft = swipeTransition.upOrLeft(this)
+ val downOrRight = swipeTransition.downOrRight(this)
// Compute the target scene depending on the current offset.
return when {
@@ -516,6 +557,22 @@
var _distance by mutableFloatStateOf(0f)
val distance: Float
get() = _distance
+
+ /** The [UserAction]s associated to this swipe. */
+ var actionUpOrLeft: UserAction = Back
+ var actionDownOrRight: UserAction = Back
+ var actionUpOrLeftNoEdge: UserAction? = null
+ var actionDownOrRightNoEdge: UserAction? = null
+
+ fun upOrLeft(scene: Scene): SceneKey? {
+ return scene.userActions[actionUpOrLeft]
+ ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
+ }
+
+ fun downOrRight(scene: Scene): SceneKey? {
+ return scene.userActions[actionDownOrRight]
+ ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
+ }
}
companion object {
@@ -526,9 +583,9 @@
private class SceneDraggableHandler(
private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
- override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) {
+ override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
gestureHandler.gestureWithPriority = this
- gestureHandler.onDragStarted()
+ gestureHandler.onDragStarted(pointersDown, startedPosition)
}
override fun onDelta(pixels: Float) {
@@ -537,7 +594,7 @@
}
}
- override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) {
+ override fun onDragStopped(velocity: Float) {
if (gestureHandler.gestureWithPriority == this) {
gestureHandler.gestureWithPriority = null
gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
@@ -580,11 +637,31 @@
// moving on to the next scene.
var gestureStartedOnNestedChild = false
+ val actionUpOrLeft =
+ Swipe(
+ direction =
+ when (gestureHandler.orientation) {
+ Orientation.Horizontal -> SwipeDirection.Left
+ Orientation.Vertical -> SwipeDirection.Up
+ },
+ pointerCount = 1,
+ )
+
+ val actionDownOrRight =
+ Swipe(
+ direction =
+ when (gestureHandler.orientation) {
+ Orientation.Horizontal -> SwipeDirection.Right
+ Orientation.Vertical -> SwipeDirection.Down
+ },
+ pointerCount = 1,
+ )
+
fun findNextScene(amount: Float): SceneKey? {
val fromScene = gestureHandler.currentScene
return when {
- amount < 0f -> fromScene.upOrLeft(gestureHandler.orientation)
- amount > 0f -> fromScene.downOrRight(gestureHandler.orientation)
+ amount < 0f -> fromScene.userActions[actionUpOrLeft]
+ amount > 0f -> fromScene.userActions[actionDownOrRight]
else -> null
}
}
@@ -625,7 +702,7 @@
onStart = {
gestureHandler.gestureWithPriority = this
priorityScene = nextScene
- gestureHandler.onDragStarted()
+ gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
},
onScroll = { offsetAvailable ->
if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
new file mode 100644
index 0000000..a68282a
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FixedSizeEdgeDetectorTest {
+ private val detector = FixedSizeEdgeDetector(30.dp)
+ private val layoutSize = IntSize(100, 100)
+ private val density = Density(1f)
+
+ @Test
+ fun horizontalEdges() {
+ fun horizontalEdge(position: Int): Edge? =
+ detector.edge(
+ layoutSize,
+ position = IntOffset(position, 0),
+ density,
+ Orientation.Horizontal,
+ )
+
+ assertThat(horizontalEdge(0)).isEqualTo(Edge.Left)
+ assertThat(horizontalEdge(30)).isEqualTo(Edge.Left)
+ assertThat(horizontalEdge(31)).isEqualTo(null)
+ assertThat(horizontalEdge(69)).isEqualTo(null)
+ assertThat(horizontalEdge(70)).isEqualTo(Edge.Right)
+ assertThat(horizontalEdge(100)).isEqualTo(Edge.Right)
+ }
+
+ @Test
+ fun verticalEdges() {
+ fun verticalEdge(position: Int): Edge? =
+ detector.edge(
+ layoutSize,
+ position = IntOffset(0, position),
+ density,
+ Orientation.Vertical,
+ )
+
+ assertThat(verticalEdge(0)).isEqualTo(Edge.Top)
+ assertThat(verticalEdge(30)).isEqualTo(Edge.Top)
+ assertThat(verticalEdge(31)).isEqualTo(null)
+ assertThat(verticalEdge(69)).isEqualTo(null)
+ assertThat(verticalEdge(70)).isEqualTo(Edge.Bottom)
+ assertThat(verticalEdge(100)).isEqualTo(Edge.Bottom)
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 6791a85..1eb3392 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -55,7 +55,8 @@
builder = scenesBuilder,
transitions = EmptyTestTransitions,
state = layoutState,
- density = Density(1f)
+ density = Density(1f),
+ edgeDetector = DefaultEdgeDetector,
)
.also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
orientation = Orientation.Vertical,
@@ -104,13 +105,13 @@
@Test
fun onDragStarted_shouldStartATransition() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
}
@Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -123,14 +124,13 @@
@Test
fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
- coroutineScope = coroutineScope,
velocity = velocityThreshold - 0.01f,
)
assertScene(currentScene = SceneA, isIdle = false)
@@ -142,14 +142,13 @@
@Test
fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
- coroutineScope = coroutineScope,
velocity = velocityThreshold,
)
assertScene(currentScene = SceneC, isIdle = false)
@@ -161,23 +160,22 @@
@Test
fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
- draggable.onDragStopped(coroutineScope = coroutineScope, velocity = 0f)
+ draggable.onDragStopped(velocity = 0f)
assertScene(currentScene = SceneA, isIdle = true)
}
@Test
fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
- coroutineScope = coroutineScope,
velocity = velocityThreshold,
)
@@ -191,7 +189,7 @@
assertScene(currentScene = SceneC, isIdle = false)
// Start a new gesture while the offset is animating
- draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
+ draggable.onDragStarted(startedPosition = Offset.Zero)
assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
}
@@ -320,7 +318,7 @@
}
@Test
fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
- draggable.onDragStopped(coroutineScope, velocityThreshold)
+ draggable.onDragStopped(velocityThreshold)
assertScene(currentScene = SceneA, isIdle = true)
}
@@ -332,7 +330,7 @@
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
- draggable.onDragStarted(coroutineScope, Offset.Zero)
+ draggable.onDragStarted(Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -344,7 +342,7 @@
assertThat(transition.progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
- draggable.onDragStopped(coroutineScope, velocityThreshold)
+ draggable.onDragStopped(velocityThreshold)
assertScene(currentScene = SceneA, isIdle = false)
nestedScrollEvents(available = offsetY10)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index df3b72a..4a6066f 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -48,6 +48,14 @@
/** The middle of the layout, in pixels. */
private val Density.middle: Offset
get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
+
+ /** The middle-top of the layout, in pixels. */
+ private val Density.middleTop: Offset
+ get() = Offset((LayoutWidth / 2).toPx(), 0f)
+
+ /** The middle-left of the layout, in pixels. */
+ private val Density.middleLeft: Offset
+ get() = Offset(0f, (LayoutHeight / 2).toPx())
}
private var currentScene by mutableStateOf(TestScenes.SceneA)
@@ -83,7 +91,13 @@
}
scene(
TestScenes.SceneC,
- userActions = mapOf(Swipe.Down to TestScenes.SceneA),
+ userActions =
+ mapOf(
+ Swipe.Down to TestScenes.SceneA,
+ Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
+ Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB,
+ Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB,
+ ),
) {
Box(Modifier.fillMaxSize())
}
@@ -242,4 +256,100 @@
assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
}
+
+ @Test
+ fun multiPointerSwipe() {
+ // Start at scene C.
+ currentScene = TestScenes.SceneC
+
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
+ // Swipe down with two fingers.
+ rule.onRoot().performTouchInput {
+ repeat(2) { i -> down(pointerId = i, middle) }
+ repeat(2) { i ->
+ moveBy(pointerId = i, Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
+ }
+ }
+
+ // We are transitioning to B because we used 2 fingers.
+ val transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneC)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the fingers and wait for the animation to end. We are back to C because we only
+ // swiped 10dp.
+ rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
+
+ @Test
+ fun defaultEdgeSwipe() {
+ // Start at scene C.
+ currentScene = TestScenes.SceneC
+
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
+ // Swipe down from the top edge.
+ rule.onRoot().performTouchInput {
+ down(middleTop)
+ moveBy(Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
+ }
+
+ // We are transitioning to B (and not A) because we started from the top edge.
+ var transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneC)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the fingers and wait for the animation to end. We are back to C because we only
+ // swiped 10dp.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+
+ // Swipe right from the left edge.
+ rule.onRoot().performTouchInput {
+ down(middleLeft)
+ moveBy(Offset(touchSlop + 10.dp.toPx(), 0f), delayMillis = 1_000)
+ }
+
+ // We are transitioning to B (and not A) because we started from the left edge.
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneC)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+
+ // Release the fingers and wait for the animation to end. We are back to C because we only
+ // swiped 10dp.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
}
diff --git a/packages/SystemUI/res/layout/connected_display_chip.xml b/packages/SystemUI/res/layout/connected_display_chip.xml
index d9df91e..f9a183d 100644
--- a/packages/SystemUI/res/layout/connected_display_chip.xml
+++ b/packages/SystemUI/res/layout/connected_display_chip.xml
@@ -41,6 +41,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
+ android:layout_marginVertical="3dp"
android:scaleType="centerInside"
android:src="@drawable/stat_sys_connected_display"
android:tint="@android:color/black" />
diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml
index 8cfcb68..3f65aa7 100644
--- a/packages/SystemUI/res/layout/connected_display_dialog.xml
+++ b/packages/SystemUI/res/layout/connected_display_dialog.xml
@@ -30,11 +30,11 @@
android:layout_width="@dimen/connected_display_dialog_logo_size"
android:layout_height="@dimen/connected_display_dialog_logo_size"
android:background="@drawable/circular_background"
- android:backgroundTint="?androidprv:attr/materialColorPrimary"
+ android:backgroundTint="?androidprv:attr/materialColorSecondary"
android:importantForAccessibility="no"
android:padding="6dp"
android:src="@drawable/stat_sys_connected_display"
- android:tint="?androidprv:attr/materialColorOnPrimary" />
+ android:tint="?androidprv:attr/materialColorOnSecondary" />
<TextView
android:id="@+id/connected_display_dialog_title"
diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java b/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java
index 8c987e3..ee70de3 100644
--- a/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java
+++ b/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java
@@ -47,9 +47,11 @@
* non six digit pin on their device
*/
public class PinShapeNonHintingView extends LinearLayout implements PinShapeInput {
-
+ private static final int RESET_STAGGER_DELAY = 40;
+ private static final int RESET_MAX_DELAY = 200;
private int mColor = Utils.getColorAttr(getContext(), PIN_SHAPES).getDefaultColor();
private int mPosition = 0;
+ private boolean mIsAnimatingReset = false;
private final PinShapeAdapter mPinShapeAdapter;
private ValueAnimator mValueAnimator = ValueAnimator.ofFloat(1f, 0f);
private Rect mFirstChildVisibleRect = new Rect();
@@ -64,8 +66,9 @@
if (getChildCount() > 2) {
View firstChild = getChildAt(0);
boolean isVisible = firstChild.getLocalVisibleRect(mFirstChildVisibleRect);
- boolean clipped = mFirstChildVisibleRect.left > 0
- || mFirstChildVisibleRect.right < firstChild.getWidth();
+ boolean clipped = mFirstChildVisibleRect.right - mFirstChildVisibleRect.left
+ < firstChild.getWidth() && firstChild.getScaleX()
+ == 1f; // Ensure that dot is not undergoing delete animation.
if (!isVisible || clipped) {
setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
return;
@@ -77,6 +80,9 @@
@Override
public void append() {
+ if (mIsAnimatingReset) {
+ return;
+ }
int size = getResources().getDimensionPixelSize(R.dimen.password_shape_size);
ImageView pinDot = new ImageView(getContext());
pinDot.setLayoutParams(new LayoutParams(size, size));
@@ -130,9 +136,20 @@
@Override
public void reset() {
+ if (mPosition == 0) {
+ return;
+ }
final int position = mPosition;
+ float baseDelay = Math.min(RESET_MAX_DELAY / position, RESET_STAGGER_DELAY);
+ mIsAnimatingReset = true;
for (int i = 0; i < position; i++) {
- delete();
+ int delayMillis = (int) (baseDelay * i);
+ postDelayed(this::delete, delayMillis);
+ // When we reach the last index, we want to send a signal that the animation is
+ // complete.
+ if (i == position - 1) {
+ postDelayed(() -> mIsAnimatingReset = false, delayMillis);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index cd8bef1..ceddee8 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -341,7 +341,16 @@
void moveToEdgeAndHide() {
mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
final PointF position = mMenuView.getMenuPosition();
- moveToPosition(getTuckedMenuPosition());
+ final PointF tuckedPosition = getTuckedMenuPosition();
+ if (Flags.floatingMenuAnimatedTuck()) {
+ flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
+ Math.signum(tuckedPosition.x - position.x) * ESCAPE_VELOCITY,
+ FLING_FRICTION_SCALAR,
+ createDefaultSpringForce(),
+ tuckedPosition.x);
+ } else {
+ moveToPosition(tuckedPosition);
+ }
// Keep the touch region let users could click extra space to pop up the menu view
// from the screen edge
@@ -353,7 +362,24 @@
void moveOutEdgeAndShow() {
mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
- mMenuView.onPositionChanged();
+ if (Flags.floatingMenuAnimatedTuck()) {
+ PointF position = mMenuView.getMenuPosition();
+ springMenuWith(DynamicAnimation.TRANSLATION_X,
+ createDefaultSpringForce(),
+ 0,
+ position.x,
+ true
+ );
+ springMenuWith(DynamicAnimation.TRANSLATION_Y,
+ createDefaultSpringForce(),
+ 0,
+ position.y,
+ true
+ );
+ } else {
+ mMenuView.onPositionChanged();
+ }
+
mMenuView.onEdgeChangedIfNeeded();
}
@@ -489,6 +515,12 @@
return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
}
+ private static SpringForce createDefaultSpringForce() {
+ return new SpringForce()
+ .setStiffness(SPRING_STIFFNESS)
+ .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO);
+ }
+
static class MenuPositionProperty
extends FloatPropertyCompat<MenuView> {
private final DynamicAnimation.ViewProperty mProperty;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index ea5a56c..92c7259 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -71,6 +71,7 @@
private final MenuAnimationController mMenuAnimationController;
private OnTargetFeaturesChangeListener mFeaturesChangeListener;
+ private OnMoveToTuckedListener mMoveToTuckedListener;
MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) {
super(context);
@@ -138,6 +139,10 @@
mFeaturesChangeListener = listener;
}
+ void setMoveToTuckedListener(OnMoveToTuckedListener listener) {
+ mMoveToTuckedListener = listener;
+ }
+
void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) {
mTargetFeaturesView.addOnItemTouchListener(listener);
}
@@ -307,8 +312,11 @@
void updateMenuMoveToTucked(boolean isMoveToTucked) {
mIsMoveToTucked = isMoveToTucked;
mMenuViewModel.updateMenuMoveToTucked(isMoveToTucked);
+ if (mMoveToTuckedListener != null) {
+ mMoveToTuckedListener.onMoveToTuckedChanged(isMoveToTucked);
+ }
- if (Flags.floatingMenuOverlapsNavBarsFlag()) {
+ if (Flags.floatingMenuOverlapsNavBarsFlag() && !Flags.floatingMenuAnimatedTuck()) {
if (isMoveToTucked) {
final float halfWidth = getMenuWidth() / 2.0f;
final boolean isOnLeftSide = mMenuAnimationController.isOnLeftSide();
@@ -428,4 +436,11 @@
*/
void onChange(List<AccessibilityTarget> newTargetFeatures);
}
+
+ /**
+ * Interface containing a callback for when MoveToTucked changes.
+ */
+ interface OnMoveToTuckedListener {
+ void onMoveToTuckedChanged(boolean moveToTucked);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
index 89ce065..4865fce 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewAppearance.java
@@ -281,7 +281,7 @@
: new float[]{radius, radius, 0.0f, 0.0f, 0.0f, 0.0f, radius, radius};
}
- private Rect getWindowAvailableBounds() {
+ public Rect getWindowAvailableBounds() {
final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
final WindowInsets windowInsets = windowMetrics.getWindowInsets();
final Insets insets = windowInsets.getInsetsIgnoringVisibility(
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index fbca022..ff3a9e3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -79,7 +79,8 @@
*/
@SuppressLint("ViewConstructor")
class MenuViewLayer extends FrameLayout implements
- ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks {
+ ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks,
+ MenuView.OnMoveToTuckedListener {
private static final int SHOW_MESSAGE_DELAY_MS = 3000;
private final WindowManager mWindowManager;
@@ -211,6 +212,7 @@
mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
mDismissAnimationController);
mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler);
+ mMenuView.setMoveToTuckedListener(this);
mMessageView = new MenuMessageView(context);
@@ -232,6 +234,10 @@
addView(mMenuView, LayerIndex.MENU_VIEW);
addView(mDismissView, LayerIndex.DISMISS_VIEW);
addView(mMessageView, LayerIndex.MESSAGE_VIEW);
+
+ if (Flags.floatingMenuAnimatedTuck()) {
+ setClipChildren(true);
+ }
}
@Override
@@ -354,6 +360,24 @@
mShouldShowDockTooltip = !hasSeenTooltip;
}
+ public void onMoveToTuckedChanged(boolean moveToTuck) {
+ if (Flags.floatingMenuOverlapsNavBarsFlag()) {
+ if (moveToTuck) {
+ final Rect bounds = mMenuViewAppearance.getWindowAvailableBounds();
+ final int[] location = getLocationOnScreen();
+ bounds.offset(
+ location[0],
+ location[1]
+ );
+
+ setClipBounds(bounds);
+ }
+ // Instead of clearing clip bounds when moveToTuck is false,
+ // wait until the spring animation finishes.
+ }
+ // Function is a no-operation if flag is disabled.
+ }
+
private void onSpringAnimationsEndAction() {
if (mShouldShowDockTooltip) {
mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
@@ -364,6 +388,11 @@
mMenuAnimationController.startTuckedAnimationPreview();
}
+ if (Flags.floatingMenuAnimatedTuck()) {
+ if (!mMenuView.isMoveToTucked()) {
+ setClipBounds(null);
+ }
+ }
if (Flags.floatingMenuImeDisplacementAnimation()) {
mMenuView.onArrivalAtPosition();
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
index 5b0bd95..0ae2e16 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FacePropertyRepository.kt
@@ -29,13 +29,16 @@
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
/** A repository for the global state of Face sensor. */
interface FacePropertyRepository {
@@ -56,7 +59,8 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val faceManager: FaceManager?
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val faceManager: FaceManager?,
) : FacePropertyRepository {
override val sensorInfo: StateFlow<FaceSensorInfo?> =
@@ -77,7 +81,9 @@
)
}
}
- faceManager?.addAuthenticatorsRegisteredCallback(callback)
+ withContext(backgroundDispatcher) {
+ faceManager?.addAuthenticatorsRegisteredCallback(callback)
+ }
awaitClose {}
}
.onEach { Log.d(TAG, "sensorProps changed: $it") }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index aa33100..0c0ed77 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -33,7 +33,9 @@
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -41,6 +43,7 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
/**
* A repository for the global state of FingerprintProperty.
@@ -67,6 +70,7 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
private val fingerprintManager: FingerprintManager?,
) : FingerprintPropertyRepository {
@@ -93,7 +97,9 @@
}
}
}
- fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
+ withContext(backgroundDispatcher) {
+ fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
+ }
awaitClose {}
}
.stateIn(
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
index b2bc06f..48d3742 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
@@ -20,4 +20,7 @@
data class SharedNotificationContainerPosition(
val top: Float = 0f,
val bottom: Float = 0f,
+
+ /** Whether any modifications to top/bottom are smoothly animated */
+ val animate: Boolean = false,
)
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
index be9a299..42bb5bb 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt
@@ -77,7 +77,7 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.asIndenting
import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.indentIfPossible
+import com.android.systemui.util.withIncreasedIndent
import com.android.wm.shell.taskview.TaskViewFactory
import dagger.Lazy
import java.io.PrintWriter
@@ -822,9 +822,9 @@
private fun findSelectionItem(si: SelectedItem, items: List<SelectionItem>): SelectionItem? =
items.firstOrNull { it.matches(si) }
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("ControlsUiControllerImpl:")
- pw.asIndenting().indentIfPossible {
+ override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ println("ControlsUiControllerImpl:")
+ withIncreasedIndent {
println("hidden: $hidden")
println("selectedItem: $selectedItem")
println("lastSelections: $lastSelections")
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index d57f31f..8b992fc 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -41,6 +41,7 @@
import android.app.trust.TrustManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
+import android.companion.virtual.VirtualDeviceManager;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -236,6 +237,12 @@
@Provides
@Singleton
+ static VirtualDeviceManager provideVirtualDeviceManager(Context context) {
+ return context.getSystemService(VirtualDeviceManager.class);
+ }
+
+ @Provides
+ @Singleton
static DeviceStateManager provideDeviceStateManager(Context context) {
return context.getSystemService(DeviceStateManager.class);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 04b2852d..a41bb2f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -54,6 +54,7 @@
import com.android.systemui.dreams.dagger.DreamModule;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FlagDependenciesModule;
import com.android.systemui.flags.FlagsModule;
import com.android.systemui.keyboard.KeyboardModule;
import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
@@ -182,6 +183,7 @@
DreamModule.class,
FalsingModule.class,
FlagsModule.class,
+ FlagDependenciesModule.class,
FooterActionsModule.class,
KeyEventRepositoryModule.class,
KeyboardModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index e96e318..e872d13 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.deviceentry.domain.interactor
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
@@ -5,6 +21,8 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import javax.inject.Inject
@@ -14,6 +32,7 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
/**
@@ -27,9 +46,11 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val repository: DeviceEntryRepository,
+ repository: DeviceEntryRepository,
private val authenticationInteractor: AuthenticationInteractor,
sceneInteractor: SceneInteractor,
+ deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
+ trustRepository: TrustRepository,
) {
/**
* Whether the device is unlocked.
@@ -73,6 +94,13 @@
initialValue = false,
)
+ // Authenticated by a TrustAgent like trusted device, location, etc or by face auth.
+ private val passivelyAuthenticated =
+ merge(
+ trustRepository.isCurrentUserTrusted,
+ deviceEntryFaceAuthRepository.isAuthenticated,
+ )
+
/**
* Whether it's currently possible to swipe up to enter the device without requiring
* authentication. This returns `false` whenever the lockscreen has been dismissed.
@@ -81,10 +109,14 @@
* UI.
*/
val canSwipeToEnter =
- combine(authenticationInteractor.authenticationMethod, isDeviceEntered) {
- authenticationMethod,
- isDeviceEntered ->
- authenticationMethod is AuthenticationMethodModel.Swipe && !isDeviceEntered
+ combine(
+ authenticationInteractor.authenticationMethod.map {
+ it == AuthenticationMethodModel.Swipe
+ },
+ passivelyAuthenticated,
+ isDeviceEntered
+ ) { isSwipeAuthMethod, passivelyAuthenticated, isDeviceEntered ->
+ (isSwipeAuthMethod || passivelyAuthenticated) && !isDeviceEntered
}
.stateIn(
scope = applicationScope,
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
index 3e2ecee..0c8dbe7 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
@@ -56,6 +56,9 @@
/** Display change event indicating a change to the given displayId has occurred. */
val displayChangeEvent: Flow<Int>
+ /** Display addition event indicating a new display has been added. */
+ val displayAdditionEvent: Flow<Display?>
+
/** Provides the current set of displays. */
val displays: Flow<Set<Display>>
@@ -126,6 +129,11 @@
override val displayChangeEvent: Flow<Int> =
allDisplayEvents.filter { it is DisplayEvent.Changed }.map { it.displayId }
+ override val displayAdditionEvent: Flow<Display?> =
+ allDisplayEvents
+ .filter { it is DisplayEvent.Added }
+ .map { displayManager.getDisplay(it.displayId) }
+
private val enabledDisplays =
allDisplayEvents
.map { getDisplays() }
diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt
index 11ed96d..cf86885 100644
--- a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt
@@ -16,6 +16,8 @@
package com.android.systemui.display.domain.interactor
+import android.companion.virtual.VirtualDeviceManager
+import android.companion.virtual.flags.Flags
import android.view.Display
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.DisplayRepository
@@ -26,6 +28,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
/** Provides information about an external connected display. */
@@ -40,6 +43,12 @@
*/
val connectedDisplayState: Flow<State>
+ /**
+ * Indicates that there is a new connected display (either an external display or a virtual
+ * device owned mirror display).
+ */
+ val connectedDisplayAddition: Flow<Unit>
+
/** Pending display that can be enabled to be used by the system. */
val pendingDisplay: Flow<PendingDisplay?>
@@ -69,6 +78,7 @@
class ConnectedDisplayInteractorImpl
@Inject
constructor(
+ private val virtualDeviceManager: VirtualDeviceManager,
keyguardRepository: KeyguardRepository,
displayRepository: DisplayRepository,
) : ConnectedDisplayInteractor {
@@ -76,13 +86,14 @@
override val connectedDisplayState: Flow<State> =
displayRepository.displays
.map { displays ->
- val externalDisplays =
- displays.filter { display -> display.type == Display.TYPE_EXTERNAL }
+ val externalDisplays = displays.filter { isExternalDisplay(it) }
- val secureExternalDisplays =
- externalDisplays.filter { it.flags and Display.FLAG_SECURE != 0 }
+ val secureExternalDisplays = externalDisplays.filter { isSecureDisplay(it) }
- if (externalDisplays.isEmpty()) {
+ val virtualDeviceMirrorDisplays =
+ displays.filter { isVirtualDeviceOwnedMirrorDisplay(it) }
+
+ if (externalDisplays.isEmpty() && virtualDeviceMirrorDisplays.isEmpty()) {
State.DISCONNECTED
} else if (!secureExternalDisplays.isEmpty()) {
State.CONNECTED_SECURE
@@ -92,6 +103,13 @@
}
.distinctUntilChanged()
+ override val connectedDisplayAddition: Flow<Unit> =
+ displayRepository.displayAdditionEvent
+ .filter {
+ it != null && (isExternalDisplay(it) || isVirtualDeviceOwnedMirrorDisplay(it))
+ }
+ .map {} // map to Unit
+
// Provides the pending display only if the lockscreen is unlocked
override val pendingDisplay: Flow<PendingDisplay?> =
displayRepository.pendingDisplay.combine(keyguardRepository.isKeyguardShowing) {
@@ -109,4 +127,17 @@
override suspend fun enable() = this@toInteractorPendingDisplay.enable()
override suspend fun ignore() = this@toInteractorPendingDisplay.ignore()
}
+
+ private fun isExternalDisplay(display: Display): Boolean {
+ return display.type == Display.TYPE_EXTERNAL
+ }
+
+ private fun isSecureDisplay(display: Display): Boolean {
+ return display.flags and Display.FLAG_SECURE != 0
+ }
+
+ private fun isVirtualDeviceOwnedMirrorDisplay(display: Display): Boolean {
+ return Flags.interactiveScreenMirror() &&
+ virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(display.displayId)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
new file mode 100644
index 0000000..730a7a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.flags
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags as Classic
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import javax.inject.Inject
+
+/** A class in which engineers can define flag dependencies */
+@SysUISingleton
+class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) :
+ FlagDependenciesBase(featureFlags, handler) {
+ override fun defineDependencies() {
+ FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
+ NotificationIconContainerRefactor.token dependsOn Classic.NOTIFICATION_SHELF_REFACTOR
+
+ // These two flags are effectively linked. We should migrate them to a single aconfig flag.
+ Classic.MIGRATE_NSSL dependsOn Classic.MIGRATE_KEYGUARD_STATUS_VIEW
+ Classic.MIGRATE_KEYGUARD_STATUS_VIEW dependsOn Classic.MIGRATE_NSSL
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
new file mode 100644
index 0000000..ae3b501
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.flags
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.Compile
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.withIncreasedIndent
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * This base class provides the helpers necessary to define dependencies between flags from the
+ * different flagging systems; classic and aconfig. This class is to be extended
+ */
+abstract class FlagDependenciesBase(
+ private val featureFlags: FeatureFlagsClassic,
+ private val handler: Handler
+) : CoreStartable {
+ protected abstract fun defineDependencies()
+
+ private val workingDependencies = mutableListOf<Dependency>()
+ private var allDependencies = emptyList<Dependency>()
+ private var unmetDependencies = emptyList<Dependency>()
+
+ override fun start() {
+ defineDependencies()
+ allDependencies = workingDependencies.toList()
+ unmetDependencies = workingDependencies.filter { !it.isMet }
+ workingDependencies.clear()
+ if (unmetDependencies.isNotEmpty()) {
+ handler.warnAboutBadFlagConfiguration(all = allDependencies, unmet = unmetDependencies)
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.asIndenting().run {
+ println("allDependencies: ${allDependencies.size}")
+ withIncreasedIndent { allDependencies.forEach(::println) }
+ println("unmetDependencies: ${unmetDependencies.size}")
+ withIncreasedIndent { unmetDependencies.forEach(::println) }
+ }
+ }
+
+ /** A dependency where enabling the `alpha` feature depends on enabling the `beta` feature */
+ class Dependency(
+ private val alphaName: String,
+ private val alphaEnabled: Boolean,
+ private val betaName: String,
+ private val betaEnabled: Boolean
+ ) {
+ val isMet = !alphaEnabled || betaEnabled
+ override fun toString(): String {
+ val isMetBullet = if (isMet) "+" else "-"
+ return "$isMetBullet $alphaName ($alphaEnabled) DEPENDS ON $betaName ($betaEnabled)"
+ }
+ }
+
+ protected infix fun UnreleasedFlag.dependsOn(other: UnreleasedFlag) =
+ addDependency(this.token, other.token)
+ protected infix fun ReleasedFlag.dependsOn(other: UnreleasedFlag) =
+ addDependency(this.token, other.token)
+ protected infix fun ReleasedFlag.dependsOn(other: ReleasedFlag) =
+ addDependency(this.token, other.token)
+ protected infix fun FlagToken.dependsOn(other: UnreleasedFlag) =
+ addDependency(this, other.token)
+ protected infix fun FlagToken.dependsOn(other: ReleasedFlag) = addDependency(this, other.token)
+ protected infix fun FlagToken.dependsOn(other: FlagToken) = addDependency(this, other)
+
+ private val UnreleasedFlag.token
+ get() = FlagToken("classic.$name", featureFlags.isEnabled(this))
+ private val ReleasedFlag.token
+ get() = FlagToken("classic.$name", featureFlags.isEnabled(this))
+
+ /** Add a dependency to the working list */
+ private fun addDependency(first: FlagToken, second: FlagToken) {
+ if (!Compile.IS_DEBUG) return // `user` builds should omit all this code
+ workingDependencies.add(
+ Dependency(first.name, first.isEnabled, second.name, second.isEnabled)
+ )
+ }
+
+ /** An interface which handles a warning about a bad flag configuration. */
+ interface Handler {
+ fun warnAboutBadFlagConfiguration(all: List<Dependency>, unmet: List<Dependency>)
+ }
+}
+
+/**
+ * A flag dependencies handler which posts a notification and logs to logcat that the configuration
+ * is invalid.
+ */
+@SysUISingleton
+class FlagDependenciesNotifier
+@Inject
+constructor(
+ private val context: Context,
+ private val notifManager: NotificationManager,
+) : FlagDependenciesBase.Handler {
+ override fun warnAboutBadFlagConfiguration(
+ all: List<FlagDependenciesBase.Dependency>,
+ unmet: List<FlagDependenciesBase.Dependency>
+ ) {
+ val title = "Invalid flag dependencies: ${unmet.size} of ${all.size}"
+ val details = unmet.joinToString("\n")
+ Log.e("FlagDependencies", "$title:\n$details")
+ val channel = NotificationChannel("FLAGS", "Flags", NotificationManager.IMPORTANCE_DEFAULT)
+ val notification =
+ Notification.Builder(context, channel.id)
+ .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
+ .setContentTitle(title)
+ .setContentText(details)
+ .setStyle(Notification.BigTextStyle().bigText(details))
+ .build()
+ notifManager.createNotificationChannel(channel)
+ notifManager.notify("flags", 0, notification)
+ }
+}
+
+@Module
+abstract class FlagDependenciesModule {
+
+ /** Inject into FlagDependencies. */
+ @Binds
+ @IntoMap
+ @ClassKey(FlagDependencies::class)
+ abstract fun bindFlagDependencies(sysui: FlagDependencies): CoreStartable
+
+ /** Bind the flag dependencies handler */
+ @Binds
+ abstract fun bindFlagDependenciesHandler(
+ handler: FlagDependenciesNotifier
+ ): FlagDependenciesBase.Handler
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/OWNERS b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
index c9d2db1..57ebccb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
@@ -10,3 +10,6 @@
alexflo@google.com
dsandler@android.com
adamcohen@google.com
+
+# Anyone in System UI can declare dependencies between flags
+per-file FlagDependencies.kt = file:../../../../../OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
index 2aa397f..ae67e60 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
@@ -16,6 +16,7 @@
package com.android.systemui.flags
+import android.os.Build
import android.util.Log
/**
@@ -25,6 +26,7 @@
* ```
* object SomeRefactor {
* const val FLAG_NAME = Flags.SOME_REFACTOR
+ * val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled)
* @JvmStatic inline val isEnabled get() = Flags.someRefactor()
* @JvmStatic inline fun isUnexpectedlyInLegacyMode() =
* RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
@@ -32,6 +34,11 @@
* RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
* }
* ```
+ *
+ * Legacy mode crashes can be disabled with the command:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
*/
@Suppress("NOTHING_TO_INLINE")
object RefactorFlagUtils {
@@ -51,8 +58,7 @@
inline fun isUnexpectedlyInLegacyMode(isEnabled: Boolean, flagName: Any): Boolean {
val inLegacyMode = !isEnabled
if (inLegacyMode) {
- val message = "New code path expects $flagName to be enabled."
- Log.wtf("RefactorFlag", message, IllegalStateException(message))
+ assertOnEngBuild("New code path expects $flagName to be enabled.")
}
return inLegacyMode
}
@@ -71,4 +77,37 @@
*/
inline fun assertInLegacyMode(isEnabled: Boolean, flagName: Any) =
check(!isEnabled) { "Legacy code path not supported when $flagName is enabled." }
+
+ /**
+ * This will [Log.wtf] with the given message, assuming [ASSERT_TAG] is loggable at that level.
+ * This means an engineer can prevent this from crashing by running the command:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
+ */
+ fun assertOnEngBuild(message: String) {
+ if (Log.isLoggable(ASSERT_TAG, Log.ASSERT)) {
+ val exception = if (Build.isDebuggable()) IllegalStateException(message) else null
+ Log.wtf(ASSERT_TAG, message, exception)
+ } else if (Log.isLoggable(STANDARD_TAG, Log.WARN)) {
+ Log.w(STANDARD_TAG, message)
+ }
+ }
+
+ /**
+ * Tag used to determine if an incorrect flag guard should crash System UI running an eng build.
+ * This is enabled by default. To disable, run:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
+ */
+ private const val ASSERT_TAG = "RefactorFlagAssert"
+
+ /** Tag used for non-crashing logs or when the [ASSERT_TAG] has been silenced. */
+ private const val STANDARD_TAG = "RefactorFlag"
+}
+
+/** An object which allows dependency tracking */
+data class FlagToken(val name: String, val isEnabled: Boolean) {
+ override fun toString(): String = "$name (${if (isEnabled) "enabled" else "disabled"})"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt
index 5bc5d0b..9da9a73 100644
--- a/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyevent/data/repository/KeyEventRepository.kt
@@ -35,7 +35,7 @@
class KeyEventRepositoryImpl
@Inject
constructor(
- val commandQueue: CommandQueue,
+ private val commandQueue: CommandQueue,
) : KeyEventRepository {
override val isPowerButtonDown: Flow<Boolean> = conflatedCallbackFlow {
val callback =
@@ -46,6 +46,7 @@
}
}
}
+ trySendWithFailureLogging(false, TAG, "init isPowerButtonDown")
commandQueue.addCallback(callback)
awaitClose { commandQueue.removeCallback(callback) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 6a0d595..c4dfe9a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -383,7 +383,12 @@
"isFaceAuthEnrolledAndEnabled"
),
Pair(keyguardRepository.isKeyguardGoingAway.isFalse(), "keyguardNotGoingAway"),
- Pair(powerInteractor.isAsleep.isFalse(), "deviceNotAsleep"),
+ Pair(
+ keyguardTransitionInteractor
+ .isInTransitionToStateWhere(KeyguardState::deviceIsAsleepInState)
+ .isFalse(),
+ "deviceNotTransitioningToAsleepState"
+ ),
Pair(
keyguardInteractor.isSecureCameraActive
.isFalse()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 3eef6aa..8d5d73f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -115,7 +115,8 @@
private var updateTransitionId: UUID? = null
init {
- // Seed with transitions signaling a boot into lockscreen state
+ // Seed with transitions signaling a boot into lockscreen state. If updating this, please
+ // also update FakeKeyguardTransitionRepository.
emitTransition(
TransitionStep(
KeyguardState.OFF,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 4d5c503..67a12b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -22,6 +22,8 @@
import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.view.ViewGroup.OnHierarchyChangeListener
+import android.view.WindowInsets
+import android.view.WindowInsets.Type
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.internal.jank.InteractionJankMonitor
@@ -242,11 +244,18 @@
}
)
+ view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
+ val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+ viewModel.topInset = insets.getInsetsIgnoringVisibility(insetTypes).top
+ insets
+ }
+
return object : DisposableHandle {
override fun dispose() {
disposableHandle.dispose()
view.removeOnLayoutChangeListener(onLayoutChangeListener)
view.setOnHierarchyChangeListener(null)
+ view.setOnApplyWindowInsetsListener(null)
childViews.clear()
}
}
@@ -288,7 +297,6 @@
oldBottom: Int
) {
val nsslPlaceholder = v.findViewById(R.id.nssl_placeholder) as View?
-
if (nsslPlaceholder != null) {
// After layout, ensure the notifications are positioned correctly
viewModel.onSharedNotificationContainerPositionChanged(
@@ -296,6 +304,11 @@
nsslPlaceholder.bottom.toFloat(),
)
}
+
+ val ksv = v.findViewById(R.id.keyguard_status_view) as View?
+ if (ksv != null) {
+ viewModel.statusViewTop = ksv.top
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 1f98082..e12da53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -65,7 +65,12 @@
*/
private val previewMode = MutableStateFlow(PreviewMode())
- public var clockControllerProvider: Provider<ClockController>? = null
+ var clockControllerProvider: Provider<ClockController>? = null
+
+ /** System insets that keyguard needs to stay out of */
+ var topInset: Int = 0
+ /** Status view top, without translation added in */
+ var statusViewTop: Int = 0
val burnInLayerVisibility: Flow<Int> =
keyguardTransitionInteractor.startedKeyguardState
@@ -102,9 +107,12 @@
scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
)
} else {
+ // Ensure the desired translation doesn't encroach on the top inset
+ val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt()
+ val translationY = -(statusViewTop - Math.max(topInset, statusViewTop + burnInY))
BurnInModel(
translationX = MathUtils.lerp(0, burnIn.translationX, interpolation).toInt(),
- translationY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt(),
+ translationY = translationY,
scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
scaleClockOnly = true,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
index 8ee1ade..e9d5dec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
@@ -24,7 +24,6 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
-import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.privacy.PrivacyChipBuilder
@@ -35,7 +34,6 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -56,8 +54,7 @@
connectedDisplayInteractor: ConnectedDisplayInteractor
) {
private val onDisplayConnectedFlow =
- connectedDisplayInteractor.connectedDisplayState
- .filter { it != State.DISCONNECTED }
+ connectedDisplayInteractor.connectedDisplayAddition
private var connectedDisplayCollectionJob: Job? = null
private lateinit var scheduler: SystemStatusAnimationScheduler
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 0190d5c..2cd5560 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -61,7 +61,9 @@
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.util.asIndenting
import com.android.systemui.util.concurrency.Execution
+import com.android.systemui.util.printCollection
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.time.SystemClock
import java.io.PrintWriter
@@ -587,10 +589,9 @@
return null
}
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("Region Samplers: ${regionSamplers.size}")
- regionSamplers.map { (_, sampler) ->
- sampler.dump(pw)
+ override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ printCollection("Region Samplers", regionSamplers.values) {
+ it.dump(this)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt
index 9a93abd..b200136 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/GutsCoordinator.kt
@@ -28,6 +28,10 @@
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager
import com.android.systemui.statusbar.notification.row.NotificationGuts
import com.android.systemui.statusbar.notification.row.NotificationGutsManager
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.println
+import com.android.systemui.util.withIncreasedIndent
import java.io.PrintWriter
import javax.inject.Inject
@@ -54,7 +58,7 @@
private var onEndLifetimeExtensionCallback: OnEndLifetimeExtensionCallback? = null
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerDumpable(this)
}
override fun attach(pipeline: NotifPipeline) {
@@ -62,16 +66,12 @@
pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
}
- override fun dump(pw: PrintWriter, args: Array<String>) {
- pw.println(" notifsWithOpenGuts: ${notifsWithOpenGuts.size}")
- for (key in notifsWithOpenGuts) {
- pw.println(" * $key")
+ override fun dump(pw: PrintWriter, args: Array<String>) = pw.asIndenting().run {
+ withIncreasedIndent {
+ printCollection("notifsWithOpenGuts", notifsWithOpenGuts)
+ printCollection("notifsExtendingLifetime", notifsExtendingLifetime)
+ println("onEndLifetimeExtensionCallback", onEndLifetimeExtensionCallback)
}
- pw.println(" notifsExtendingLifetime: ${notifsExtendingLifetime.size}")
- for (key in notifsExtendingLifetime) {
- pw.println(" * $key")
- }
- pw.println(" onEndLifetimeExtensionCallback: $onEndLifetimeExtensionCallback")
}
private val mLifetimeExtender: NotifLifetimeExtender = object : NotifLifetimeExtender {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
index 8bce5b0..5e5f2a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
@@ -20,6 +20,8 @@
import android.util.ArrayMap
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
import java.io.PrintWriter
class NotifCollectionInconsistencyTracker(val logger: NotifCollectionLogger) {
@@ -104,15 +106,9 @@
}
}
- fun dump(pw: PrintWriter) {
- pw.println("notificationsWithoutRankings: ${notificationsWithoutRankings.size}")
- for (key in notificationsWithoutRankings) {
- pw.println("\t * : $key")
- }
- pw.println("missingNotifications: ${missingNotifications.size}")
- for (key in missingNotifications) {
- pw.println("\t * : $key")
- }
+ fun dump(pw: PrintWriter) = pw.asIndenting().run {
+ printCollection("notificationsWithoutRankings", notificationsWithoutRankings)
+ printCollection("missingNotifications", missingNotifications)
}
private var attached: Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
index febc011..7b0a28a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/SelfTrackingLifetimeExtender.kt
@@ -5,6 +5,10 @@
import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.println
+import com.android.systemui.util.withIncreasedIndent
import java.io.PrintWriter
/**
@@ -104,9 +108,10 @@
mCallback = callback
}
- final override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("LifetimeExtender: $name:")
- pw.println(" mEntriesExtended: ${mEntriesExtended.size}")
- mEntriesExtended.forEach { pw.println(" * ${it.key}") }
+ final override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ println("LifetimeExtender", name)
+ withIncreasedIndent {
+ printCollection("mEntriesExtended", mEntriesExtended.keys)
+ }
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
index c873e6a..58712bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/DebugModeFilterProvider.kt
@@ -26,6 +26,9 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.util.Assert
import com.android.systemui.util.ListenerSet
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.println
import java.io.PrintWriter
import javax.inject.Inject
@@ -86,12 +89,9 @@
return entry.sbn.packageName !in allowedPackages
}
- override fun dump(pw: PrintWriter, args: Array<out String>) {
- pw.println("initialized: ${listeners.isNotEmpty()}")
- pw.println("allowedPackages: ${allowedPackages.size}")
- allowedPackages.forEachIndexed { i, pkg ->
- pw.println(" [$i]: $pkg")
- }
+ override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
+ println("initialized", listeners.isNotEmpty())
+ printCollection("allowedPackages", allowedPackages)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
index 78e9a74..9326d33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
@@ -22,7 +22,7 @@
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.util.asIndenting
-import com.android.systemui.util.withIncreasedIndent
+import com.android.systemui.util.printCollection
import java.io.PrintWriter
import javax.inject.Inject
@@ -49,11 +49,7 @@
}
override fun dump(pw: PrintWriter, args: Array<out String>) =
- pw.asIndenting().run {
- println("non-dismissible entries: ${nonDismissableEntryKeys.size}")
-
- withIncreasedIndent { nonDismissableEntryKeys.forEach(this::println) }
- }
+ pw.asIndenting().run { printCollection("non-dismissible entries", nonDismissableEntryKeys) }
companion object {
private const val TAG = "NotificationDismissibilityProvider"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
index 94e70e5..7e6044e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
@@ -17,13 +17,19 @@
package com.android.systemui.statusbar.notification.footer.shared
import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the FooterView refactor flag state. */
@Suppress("NOTHING_TO_INLINE")
object FooterViewRefactor {
+ /** The aconfig flag name */
const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index db7f46e..aca8b64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -26,6 +26,7 @@
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.ListenerSet
import com.android.systemui.util.asIndenting
+import com.android.systemui.util.println
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.withIncreasedIndent
@@ -229,13 +230,13 @@
}
override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run {
- println("isLockedOrLocking=$isLockedOrLocking")
+ println("isLockedOrLocking", isLockedOrLocking)
withIncreasedIndent {
- println("keyguardStateController.isShowing=${keyguardStateController.isShowing}")
- println("statusBarStateController.currentOrUpcomingState=" +
- "${statusBarStateController.currentOrUpcomingState}")
+ println("keyguardStateController.isShowing", keyguardStateController.isShowing)
+ println("statusBarStateController.currentOrUpcomingState",
+ statusBarStateController.currentOrUpcomingState)
}
- println("hideSilentNotificationsOnLockscreen=$hideSilentNotificationsOnLockscreen")
+ println("hideSilentNotificationsOnLockscreen", hideSilentNotificationsOnLockscreen)
}
private val isLockedOrLocking get() =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
index 79bdd1f..e6deb8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
@@ -117,11 +117,6 @@
@WorkerThread
override fun updateIcon(drawableConsumer: NotificationDrawableConsumer, icon: Icon?): Runnable {
- if (this.drawableConsumer != null && this.drawableConsumer != drawableConsumer) {
- Log.wtf(TAG, "A consumer is already set for this iconManager.")
- return Runnable {}
- }
-
this.drawableConsumer = drawableConsumer
this.lastLoadingJob?.cancel()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 60e75ff..6528cef3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -118,6 +118,7 @@
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
private NotificationViewWrapper mHeadsUpWrapper;
+ @Nullable private NotificationViewWrapper mShownWrapper = null;
private final HybridGroupManager mHybridGroupManager;
private int mClipTopAmount;
private int mContentHeight;
@@ -417,6 +418,8 @@
mContractedChild = child;
mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
mContainingNotification);
+ // The contracted wrapper has changed. If this is the shown wrapper, we need to update it.
+ updateShownWrapper(mVisibleType);
}
private NotificationViewWrapper getWrapperForView(View child) {
@@ -480,6 +483,8 @@
if (mContainingNotification != null) {
applySystemActions(mExpandedChild, mContainingNotification.getEntry());
}
+ // The expanded wrapper has changed. If this is the shown wrapper, we need to update it.
+ updateShownWrapper(mVisibleType);
}
/**
@@ -530,6 +535,8 @@
if (mContainingNotification != null) {
applySystemActions(mHeadsUpChild, mContainingNotification.getEntry());
}
+ // The heads up wrapper has changed. If this is the shown wrapper, we need to update it.
+ updateShownWrapper(mVisibleType);
}
@Override
@@ -886,6 +893,7 @@
forceUpdateVisibility(VISIBLE_TYPE_EXPANDED, mExpandedChild, mExpandedWrapper);
forceUpdateVisibility(VISIBLE_TYPE_HEADSUP, mHeadsUpChild, mHeadsUpWrapper);
forceUpdateVisibility(VISIBLE_TYPE_SINGLELINE, mSingleLineView, mSingleLineView);
+ updateShownWrapper(mVisibleType);
fireExpandedVisibleListenerIfVisible();
// forceUpdateVisibilities cancels outstanding animations without updating the
// mAnimationStartVisibleType. Do so here instead.
@@ -967,6 +975,7 @@
mHeadsUpChild, mHeadsUpWrapper);
updateViewVisibility(visibleType, VISIBLE_TYPE_SINGLELINE,
mSingleLineView, mSingleLineView);
+ updateShownWrapper(visibleType);
fireExpandedVisibleListenerIfVisible();
// updateViewVisibilities cancels outstanding animations without updating the
// mAnimationStartVisibleType. Do so here instead.
@@ -980,6 +989,28 @@
}
}
+ /**
+ * Called when the currently shown wrapper is potentially affected by a change to the
+ * {mVisibleType} or the user-visibility of this view.
+ *
+ * @see View#isShown()
+ */
+ private void updateShownWrapper(int visibleType) {
+ final NotificationViewWrapper shownWrapper = isShown() ? getVisibleWrapper(visibleType)
+ : null;
+
+ if (mShownWrapper != shownWrapper) {
+ NotificationViewWrapper hiddenWrapper = mShownWrapper;
+ mShownWrapper = shownWrapper;
+ if (hiddenWrapper != null) {
+ hiddenWrapper.onContentShown(false);
+ }
+ if (shownWrapper != null) {
+ shownWrapper.onContentShown(true);
+ }
+ }
+ }
+
private void animateToVisibleType(int visibleType) {
final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
@@ -990,6 +1021,7 @@
mAnimationStartVisibleType = mVisibleType;
shownView.transformFrom(hiddenView);
getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
+ updateShownWrapper(visibleType);
hiddenView.transformTo(shownView, new Runnable() {
@Override
public void run() {
@@ -1837,6 +1869,7 @@
@Override
public void onVisibilityAggregated(boolean isVisible) {
super.onVisibilityAggregated(isVisible);
+ updateShownWrapper(mVisibleType);
if (isVisible) {
fireExpandedVisibleListenerIfVisible();
}
@@ -2217,6 +2250,21 @@
}
@VisibleForTesting
+ protected NotificationViewWrapper getContractedWrapper() {
+ return mContractedWrapper;
+ }
+
+ @VisibleForTesting
+ protected NotificationViewWrapper getExpandedWrapper() {
+ return mExpandedWrapper;
+ }
+
+ @VisibleForTesting
+ protected NotificationViewWrapper getHeadsUpWrapper() {
+ return mHeadsUpWrapper;
+ }
+
+ @VisibleForTesting
protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) {
mContractedWrapper = contractedWrapper;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
index acd6cc6..990adf7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapper.java
@@ -68,13 +68,11 @@
}
@Override
- public void setVisible(boolean visible) {
- super.setVisible(visible);
-
+ public void onContentShown(boolean shown) {
+ super.onContentShown(shown);
BigPictureIconManager imageManager = mRow.getBigPictureIconManager();
if (imageManager != null) {
- // TODO(b/283082473) call it a bit earlier for true, as soon as the row starts to expand
- imageManager.onViewShown(visible);
+ imageManager.onViewShown(shown);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index cdf178e..50f3e78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -311,6 +311,17 @@
}
/**
+ * Called when the user-visibility of this content wrapper has changed.
+ *
+ * @param shown true if the content of this wrapper is user-visible, meaning that the wrapped
+ * view and all of its ancestors are visible.
+ *
+ * @see View#isShown()
+ */
+ public void onContentShown(boolean shown) {
+ }
+
+ /**
* Called to indicate this view is removed
*/
public void setRemoved() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
index dee609c..a08af75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
@@ -16,13 +16,19 @@
package com.android.systemui.statusbar.notification.shared
import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the NotificationIconContainer refactor flag state. */
@Suppress("NOTHING_TO_INLINE")
object NotificationIconContainerRefactor {
+ /** The aconfig flag name */
const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
/** Is the refactor enabled? */
@JvmStatic
inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 8babcc2..1774000 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -4963,7 +4963,7 @@
// Avoid Flicking during clear all
// when the shade finishes closing, onExpansionStopped will call
// resetScrollPosition to setOwnScrollY to 0
- if (mAmbientState.isClosing()) {
+ if (mAmbientState.isClosing() || mAmbientState.isClearAllInProgress()) {
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 2af7181..6785da4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -59,10 +59,8 @@
launch {
viewModel.position.collect {
- controller.updateTopPadding(
- it.top,
- controller.isAddOrRemoveAnimationPending()
- )
+ val animate = it.animate || controller.isAddOrRemoveAnimationPending()
+ controller.updateTopPadding(it.top, animate)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 1229cb9..b86b5dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -25,6 +25,7 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -118,8 +119,15 @@
}
}
} else {
- interactor.topPosition.map { top ->
- keyguardInteractor.sharedNotificationContainerPosition.value.copy(top = top)
+ interactor.topPosition.sample(shadeInteractor.qsExpansion, ::Pair).map {
+ (top, qsExpansion) ->
+ // When QS expansion > 0, it should directly set the top padding so do not
+ // animate it
+ val animate = qsExpansion == 0f
+ keyguardInteractor.sharedNotificationContainerPosition.value.copy(
+ top = top,
+ animate = animate
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt b/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
index 018ef96..5b0943a 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
@@ -18,6 +18,7 @@
import android.util.IndentingPrintWriter
import android.view.View
+import com.android.systemui.Dumpable
import java.io.PrintWriter
/**
@@ -56,13 +57,28 @@
}
/** Print a line which is '$label=$value' */
-fun IndentingPrintWriter.println(label: String, value: Any) =
+fun IndentingPrintWriter.println(label: String, value: Any?) =
append(label).append('=').println(value)
-/** Return a readable string for the visibility */
-fun visibilityString(@View.Visibility visibility: Int): String = when (visibility) {
- View.GONE -> "gone"
- View.VISIBLE -> "visible"
- View.INVISIBLE -> "invisible"
- else -> "unknown:$visibility"
+@JvmOverloads
+inline fun <T> IndentingPrintWriter.printCollection(
+ label: String,
+ collection: Collection<T>,
+ printer: IndentingPrintWriter.(T) -> Unit = IndentingPrintWriter::println,
+) {
+ append(label).append(": ").println(collection.size)
+ withIncreasedIndent { collection.forEach { printer(it) } }
}
+
+fun <T : Dumpable> IndentingPrintWriter.dumpCollection(label: String, collection: Collection<T>) {
+ printCollection(label, collection) { it.dump(this, emptyArray()) }
+}
+
+/** Return a readable string for the visibility */
+fun visibilityString(@View.Visibility visibility: Int): String =
+ when (visibility) {
+ View.GONE -> "gone"
+ View.VISIBLE -> "visible"
+ View.INVISIBLE -> "invisible"
+ else -> "unknown:$visibility"
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index 284c273..728102d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -241,9 +241,13 @@
throws RemoteException {
enableWindowMagnificationWithoutAnimation();
+ // Wait for Rects updated.
+ waitForIdleSync();
+ View mirrorView = mWindowManager.getAttachedView();
final float targetScale = 1.0f;
- final float targetCenterX = DEFAULT_CENTER_X + 100;
- final float targetCenterY = DEFAULT_CENTER_Y + 100;
+ // Move the magnifier to the top left corner, within the boundary
+ final float targetCenterX = mirrorView.getWidth() / 2.0f;
+ final float targetCenterY = mirrorView.getHeight() / 2.0f;
Mockito.reset(mSpyController);
mInstrumentation.runOnMainSync(() -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index 2e75480..834dccb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -26,6 +26,9 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import android.graphics.PointF;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
@@ -39,6 +42,7 @@
import androidx.dynamicanimation.animation.SpringForce;
import androidx.test.filters.SmallTest;
+import com.android.systemui.Flags;
import com.android.systemui.Prefs;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.settings.SecureSettings;
@@ -70,6 +74,10 @@
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Mock
private AccessibilityManager mAccessibilityManager;
@@ -223,6 +231,24 @@
verifyZeroInteractions(onSpringAnimationsEndCallback);
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK)
+ public void tuck_animates() {
+ mMenuAnimationController.cancelAnimations();
+ mMenuAnimationController.moveToEdgeAndHide();
+ assertThat(mMenuAnimationController.getAnimation(
+ DynamicAnimation.TRANSLATION_X).isRunning()).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_ANIMATED_TUCK)
+ public void untuck_animates() {
+ mMenuAnimationController.cancelAnimations();
+ mMenuAnimationController.moveOutEdgeAndShow();
+ assertThat(mMenuAnimationController.getAnimation(
+ DynamicAnimation.TRANSLATION_X).isRunning()).isTrue();
+ }
+
private void setupAndRunSpringAnimations() {
final float stiffness = 700f;
final float dampingRatio = 0.85f;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
index 0da7b4a..c14ad6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryImplTest.kt
@@ -32,6 +32,8 @@
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -54,18 +56,20 @@
@JvmField @Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private lateinit var underTest: FacePropertyRepository
+ private lateinit var dispatcher: TestDispatcher
private lateinit var testScope: TestScope
@Captor private lateinit var callback: ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback>
@Mock private lateinit var faceManager: FaceManager
@Before
fun setup() {
- testScope = TestScope()
+ dispatcher = StandardTestDispatcher()
+ testScope = TestScope(dispatcher)
underTest = createRepository(faceManager)
}
private fun createRepository(manager: FaceManager? = faceManager) =
- FacePropertyRepositoryImpl(testScope.backgroundScope, manager)
+ FacePropertyRepositoryImpl(testScope.backgroundScope, dispatcher, manager)
@Test
fun whenFaceManagerIsNotPresentIsNull() =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index ed9ae5e..dc438d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -65,7 +65,11 @@
val dispatcher = StandardTestDispatcher()
testScope = TestScope(dispatcher)
repository =
- FingerprintPropertyRepositoryImpl(testScope.backgroundScope, fingerprintManager)
+ FingerprintPropertyRepositoryImpl(
+ testScope.backgroundScope,
+ dispatcher,
+ fingerprintManager
+ )
testScope.runCurrent()
verify(fingerprintManager)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index c13fde7..aebadc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -1,3 +1,19 @@
+/*
+ * 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.deviceentry.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -6,6 +22,8 @@
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -24,6 +42,8 @@
private val utils = SceneTestUtils(this)
private val testScope = utils.testScope
private val repository: FakeDeviceEntryRepository = utils.deviceEntryRepository
+ private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
+ private val trustRepository = FakeTrustRepository()
private val sceneInteractor = utils.sceneInteractor()
private val authenticationInteractor = utils.authenticationInteractor()
private val underTest =
@@ -31,6 +51,8 @@
repository = repository,
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
+ faceAuthRepository = faceAuthRepository,
+ trustRepository = trustRepository,
)
@Test
@@ -171,6 +193,40 @@
}
@Test
+ fun canSwipeToEnter_whenTrustedByTrustManager_isTrue() =
+ testScope.runTest {
+ val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
+ utils.authenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ switchToScene(SceneKey.Lockscreen)
+ assertThat(canSwipeToEnter).isFalse()
+
+ trustRepository.setCurrentUserTrusted(true)
+ runCurrent()
+ faceAuthRepository.isAuthenticated.value = false
+
+ assertThat(canSwipeToEnter).isTrue()
+ }
+
+ @Test
+ fun canSwipeToEnter_whenAuthenticatedByFace_isTrue() =
+ testScope.runTest {
+ val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
+ utils.authenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ switchToScene(SceneKey.Lockscreen)
+ assertThat(canSwipeToEnter).isFalse()
+
+ faceAuthRepository.isAuthenticated.value = true
+ runCurrent()
+ trustRepository.setCurrentUserTrusted(false)
+
+ assertThat(canSwipeToEnter).isTrue()
+ }
+
+ @Test
fun isAuthenticationRequired_lockedAndSecured_true() =
testScope.runTest {
utils.deviceEntryRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
index 511562f..d80dd76 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
@@ -390,6 +390,17 @@
assertThat(pendingDisplay!!.id).isEqualTo(1)
}
+ @Test
+ fun onDisplayAdded_emitsDisplayAdditionEvent() =
+ testScope.runTest {
+ val display by lastDisplayAdditionEvent()
+
+ sendOnDisplayAdded(1, TYPE_EXTERNAL)
+
+ assertThat(display!!.displayId).isEqualTo(1)
+ assertThat(display!!.type).isEqualTo(TYPE_EXTERNAL)
+ }
+
private fun Iterable<Display>.ids(): List<Int> = map { it.displayId }
// Wrapper to capture the displayListener.
@@ -411,6 +422,12 @@
return flowValue
}
+ private fun TestScope.lastDisplayAdditionEvent(): FlowValue<Display?> {
+ val flowValue = collectLastValue(displayRepository.displayAdditionEvent)
+ captureAddedRemovedListener()
+ return flowValue
+ }
+
private fun captureAddedRemovedListener() {
verify(displayManager)
.registerDisplayListener(
@@ -423,9 +440,17 @@
)
)
}
+
+ private fun sendOnDisplayAdded(id: Int, displayType: Int) {
+ val mockDisplay = display(id = id, type = displayType)
+ whenever(displayManager.getDisplay(eq(id))).thenReturn(mockDisplay)
+ displayListener.value.onDisplayAdded(id)
+ }
+
private fun sendOnDisplayAdded(id: Int) {
displayListener.value.onDisplayAdded(id)
}
+
private fun sendOnDisplayRemoved(id: Int) {
displayListener.value.onDisplayRemoved(id)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt
index 26ee094..0db3de2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt
@@ -16,11 +16,14 @@
package com.android.systemui.display.domain.interactor
+import android.companion.virtual.VirtualDeviceManager
+import android.companion.virtual.flags.Flags.FLAG_INTERACTIVE_SCREEN_MIRROR
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.Display
import android.view.Display.TYPE_EXTERNAL
import android.view.Display.TYPE_INTERNAL
+import android.view.Display.TYPE_VIRTUAL
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.FlowValue
@@ -31,14 +34,20 @@
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
@@ -46,14 +55,22 @@
@SmallTest
class ConnectedDisplayInteractorTest : SysuiTestCase() {
+ private val virtualDeviceManager = mock<VirtualDeviceManager>()
+
private val fakeDisplayRepository = FakeDisplayRepository()
private val fakeKeyguardRepository = FakeKeyguardRepository()
private val connectedDisplayStateProvider: ConnectedDisplayInteractor =
- ConnectedDisplayInteractorImpl(fakeKeyguardRepository, fakeDisplayRepository)
+ ConnectedDisplayInteractorImpl(
+ virtualDeviceManager,
+ fakeKeyguardRepository,
+ fakeDisplayRepository
+ )
private val testScope = TestScope(UnconfinedTestDispatcher())
@Before
fun setup() {
+ mSetFlagsRule.disableFlags(FLAG_INTERACTIVE_SCREEN_MIRROR)
+ whenever(virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(anyInt())).thenReturn(false)
fakeKeyguardRepository.setKeyguardShowing(false)
}
@@ -137,6 +154,96 @@
}
@Test
+ fun displayState_virtualDeviceOwnedMirrorVirtualDisplay_connected() =
+ testScope.runTest {
+ mSetFlagsRule.enableFlags(FLAG_INTERACTIVE_SCREEN_MIRROR)
+ whenever(virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(anyInt()))
+ .thenReturn(true)
+ val value by lastValue()
+
+ fakeDisplayRepository.emit(setOf(display(type = TYPE_VIRTUAL)))
+
+ assertThat(value).isEqualTo(State.CONNECTED)
+ }
+
+ @Test
+ fun displayState_virtualDeviceUnownedMirrorVirtualDisplay_disconnected() =
+ testScope.runTest {
+ val value by lastValue()
+
+ fakeDisplayRepository.emit(setOf(display(type = TYPE_VIRTUAL)))
+
+ assertThat(value).isEqualTo(State.DISCONNECTED)
+ }
+
+ @Test
+ fun virtualDeviceOwnedMirrorVirtualDisplay_emitsConnectedDisplayAddition() =
+ testScope.runTest {
+ mSetFlagsRule.enableFlags(FLAG_INTERACTIVE_SCREEN_MIRROR)
+ whenever(virtualDeviceManager.isVirtualDeviceOwnedMirrorDisplay(anyInt()))
+ .thenReturn(true)
+ var count = 0
+ val job =
+ connectedDisplayStateProvider.connectedDisplayAddition
+ .onEach { count++ }
+ .launchIn(this)
+
+ fakeDisplayRepository.emit(display(type = TYPE_VIRTUAL))
+
+ runCurrent()
+ assertThat(count).isEqualTo(1)
+ job.cancel()
+ }
+
+ @Test
+ fun virtualDeviceUnownedMirrorVirtualDisplay_doesNotEmitConnectedDisplayAddition() =
+ testScope.runTest {
+ var count = 0
+ val job =
+ connectedDisplayStateProvider.connectedDisplayAddition
+ .onEach { count++ }
+ .launchIn(this)
+
+ fakeDisplayRepository.emit(display(type = TYPE_VIRTUAL))
+
+ runCurrent()
+ assertThat(count).isEqualTo(0)
+ job.cancel()
+ }
+
+ @Test
+ fun externalDisplay_emitsConnectedDisplayAddition() =
+ testScope.runTest {
+ var count = 0
+ val job =
+ connectedDisplayStateProvider.connectedDisplayAddition
+ .onEach { count++ }
+ .launchIn(this)
+
+ fakeDisplayRepository.emit(display(type = TYPE_EXTERNAL))
+
+ runCurrent()
+ assertThat(count).isEqualTo(1)
+ job.cancel()
+ }
+
+ @Test
+ fun internalDisplay_doesNotEmitConnectedDisplayAddition() =
+ testScope.runTest {
+ var count = 0
+ val job =
+ connectedDisplayStateProvider.connectedDisplayAddition
+ .onEach { count++ }
+ .launchIn(this)
+
+ fakeDisplayRepository.emit(display(type = TYPE_INTERNAL))
+
+ runCurrent()
+ assertThat(count).isEqualTo(0)
+ job.cancel()
+ }
+
+ @Test
fun pendingDisplay_propagated() =
testScope.runTest {
val value by lastPendingDisplay()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt
new file mode 100644
index 0000000..936aa8b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.flags
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.io.PrintWriter
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FlagDependenciesTest : SysuiTestCase() {
+ @Test
+ fun testRelease() {
+ testFlagDependencies(teamfood = false).start()
+ }
+
+ @Test
+ fun testTeamfood() {
+ testFlagDependencies(teamfood = true).start()
+ }
+
+ private fun testFlagDependencies(teamfood: Boolean) =
+ FlagDependencies(TestFeatureFlags(teamfood = teamfood), TestHandler())
+
+ private class TestHandler : FlagDependenciesBase.Handler {
+ override fun warnAboutBadFlagConfiguration(
+ all: List<FlagDependenciesBase.Dependency>,
+ unmet: List<FlagDependenciesBase.Dependency>
+ ) {
+ val title = "${unmet.size} invalid of ${all.size} flag dependencies"
+ val details = unmet.joinToString("\n")
+ fail("$title:\n$details")
+ }
+ }
+
+ private class TestFeatureFlags(val teamfood: Boolean) : FeatureFlagsClassic {
+ private val unsupported: Nothing
+ get() = fail("Unsupported")
+
+ override fun isEnabled(flag: ReleasedFlag): Boolean = true
+ override fun isEnabled(flag: UnreleasedFlag): Boolean = teamfood && flag.teamfood
+ override fun isEnabled(flag: ResourceBooleanFlag): Boolean = unsupported
+ override fun isEnabled(flag: SysPropBooleanFlag): Boolean = unsupported
+ override fun getString(flag: StringFlag): String = unsupported
+ override fun getString(flag: ResourceStringFlag): String = unsupported
+ override fun getInt(flag: IntFlag): Int = unsupported
+ override fun getInt(flag: ResourceIntFlag): Int = unsupported
+ override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) = unsupported
+ override fun removeListener(listener: FlagListenable.Listener) = unsupported
+ override fun dump(writer: PrintWriter, args: Array<out String>?) = unsupported
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 00951c3..f0ff77e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -12,7 +12,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
@@ -45,7 +44,6 @@
private val keyguardTransitionRepository = FakeKeyguardTransitionRepository()
private lateinit var powerInteractor: PowerInteractor
-
@Mock private lateinit var globalWindowManager: GlobalWindowManager
private lateinit var resourceTrimmer: ResourceTrimmer
@@ -181,8 +179,10 @@
@Test
fun keyguardTransitionsToGone_trimsFontCache() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
verify(globalWindowManager, times(1))
.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
@@ -194,8 +194,10 @@
fun keyguardTransitionsToGone_flagDisabled_doesNotTrimFontCache() =
testScope.runTest {
featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
// Memory hidden should still be called.
verify(globalWindowManager, times(1))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 9bb2434..8d9bc75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
@@ -32,7 +32,6 @@
import android.hardware.face.FaceSensorProperties
import android.hardware.face.FaceSensorPropertiesInternal
import android.os.CancellationSignal
-import android.util.Log
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -637,7 +636,19 @@
@Test
fun authenticateDoesNotRunWhenDeviceIsGoingToSleep() =
- testScope.runTest { testGatingCheckForFaceAuth { powerInteractor.setAsleepForTest() } }
+ testScope.runTest {
+ testGatingCheckForFaceAuth {
+ powerInteractor.setAsleepForTest()
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ )
+ )
+ runCurrent()
+ }
+ }
@Test
fun authenticateDoesNotRunWhenSecureCameraIsActive() =
@@ -733,13 +744,10 @@
allPreconditionsToRunFaceAuthAreTrue()
- Log.i("TEST", "started waking")
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.OFF,
- transitionState = TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OFF,
+ testScope
)
runCurrent()
keyguardTransitionRepository.sendTransitionStep(
@@ -751,15 +759,11 @@
)
runCurrent()
- Log.i("TEST", "sending display off")
displayRepository.emit(setOf(display(0, 0, Display.DEFAULT_DISPLAY, Display.STATE_OFF)))
displayRepository.emitDisplayChangeEvent(Display.DEFAULT_DISPLAY)
- Log.i("TEST", "sending step")
-
runCurrent()
- Log.i("TEST", "About to assert if face auth can run.")
assertThat(canFaceAuthRun()).isTrue()
}
@@ -768,12 +772,10 @@
testScope.runTest {
testGatingCheckForFaceAuth {
powerInteractor.onFinishedWakingUp()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
- transitionState = TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
)
runCurrent()
@@ -923,7 +925,19 @@
@Test
fun detectDoesNotRunWhenDeviceSleepingStartingToSleep() =
- testScope.runTest { testGatingCheckForDetect { powerInteractor.setAsleepForTest() } }
+ testScope.runTest {
+ testGatingCheckForDetect {
+ powerInteractor.setAsleepForTest()
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ )
+ )
+ runCurrent()
+ }
+ }
@Test
fun detectDoesNotRunWhenSecureCameraIsActive() =
@@ -1016,14 +1030,11 @@
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromDozing() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.DOZING,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1031,14 +1042,11 @@
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromAod() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1046,14 +1054,11 @@
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromLockscreen() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1061,14 +1066,11 @@
@Test
fun schedulesFaceManagerWatchdogWhenKeyguardIsGoneFromBouncer() =
testScope.runTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.PRIMARY_BOUNCER,
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.PRIMARY_BOUNCER,
+ to = KeyguardState.GONE,
+ testScope
)
-
runCurrent()
verify(faceManager).scheduleWatchdog()
}
@@ -1251,6 +1253,11 @@
keyguardRepository.setKeyguardShowing(true)
displayRepository.emit(setOf(display(0, 0, Display.DEFAULT_DISPLAY, Display.STATE_ON)))
displayRepository.emitDisplayChangeEvent(Display.DEFAULT_DISPLAY)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
+ )
runCurrent()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt
new file mode 100644
index 0000000..bbe45c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyEventRepositoryTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.keyguard.data.repository
+
+import android.view.KeyEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyevent.data.repository.KeyEventRepositoryImpl
+import com.android.systemui.statusbar.CommandQueue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyEventRepositoryTest : SysuiTestCase() {
+ private lateinit var underTest: KeyEventRepositoryImpl
+ @Mock private lateinit var commandQueue: CommandQueue
+ @Captor private lateinit var commandQueueCallbacks: ArgumentCaptor<CommandQueue.Callbacks>
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ testScope = TestScope()
+ underTest = KeyEventRepositoryImpl(commandQueue)
+ }
+
+ @Test
+ fun isPowerButtonDown_initialValueFalse() =
+ testScope.runTest {
+ val isPowerButtonDown by collectLastValue(underTest.isPowerButtonDown)
+ runCurrent()
+ assertThat(isPowerButtonDown).isFalse()
+ }
+
+ @Test
+ fun isPowerButtonDown_onChange() =
+ testScope.runTest {
+ val isPowerButtonDown by collectLastValue(underTest.isPowerButtonDown)
+ runCurrent()
+ verify(commandQueue).addCallback(commandQueueCallbacks.capture())
+ commandQueueCallbacks.value.handleSystemKey(
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_POWER)
+ )
+ assertThat(isPowerButtonDown).isTrue()
+
+ commandQueueCallbacks.value.handleSystemKey(
+ KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_POWER)
+ )
+ assertThat(isPowerButtonDown).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index e87adf5..e75f557 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -26,8 +26,6 @@
import com.android.systemui.keyguard.shared.model.DismissAction
import com.android.systemui.keyguard.shared.model.KeyguardDone
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.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -179,8 +177,10 @@
assertThat(executeDismissAction).isNull()
// WHEN the keyguard is GONE
- transitionRepository.sendTransitionStep(
- TransitionStep(to = KeyguardState.GONE, transitionState = TransitionState.FINISHED)
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
assertThat(executeDismissAction).isNotNull()
}
@@ -198,11 +198,10 @@
willAnimateOnLockscreen = true,
)
)
- transitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.AOD,
- transitionState = TransitionState.FINISHED,
- )
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ testScope
)
assertThat(resetDismissAction).isEqualTo(Unit)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
index 0c74a38..98f0211 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -21,7 +21,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlags
@@ -29,7 +28,7 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -265,17 +264,17 @@
underTest.onLongPress()
assertThat(isMenuVisible).isTrue()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.GONE,
- ),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
assertThat(isMenuVisible).isFalse()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.LOCKSCREEN,
- ),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
)
assertThat(isMenuVisible).isFalse()
}
@@ -312,10 +311,10 @@
keyguardState: KeyguardState = KeyguardState.LOCKSCREEN,
isQuickSettingsVisible: Boolean = false,
) {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = keyguardState,
- ),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = keyguardState,
+ testScope = testScope
)
keyguardRepository.setQuickSettingsVisible(isVisible = isQuickSettingsVisible)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index 16f2fa2..6eed427 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -231,12 +231,10 @@
surfaceBehindIsAnimatingFlow.emit(true)
runCurrent()
- transitionRepository.sendTransitionStep(
- TransitionStep(
- transitionState = TransitionState.FINISHED,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- )
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope
)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
index 3efe382..a04ea2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt
@@ -72,7 +72,7 @@
onFinish = { 10f },
)
var animationValues = collectLastValue(flow)
- repository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+ repository.sendTransitionStep(step(1f, TransitionState.FINISHED), validateStep = false)
assertThat(animationValues()).isEqualTo(10f)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 4f545cb..b80771f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -179,6 +179,8 @@
val translationY by collectLastValue(underTest.translationY)
val scale by collectLastValue(underTest.scale)
+ underTest.statusViewTop = 100
+
// Set to dozing (on AOD)
dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
// Trigger a change to the burn-in model
@@ -200,6 +202,37 @@
}
@Test
+ fun translationAndScaleFromBurnFullyDozingStaysOutOfTopInset() =
+ testScope.runTest {
+ val translationX by collectLastValue(underTest.translationX)
+ val translationY by collectLastValue(underTest.translationY)
+ val scale by collectLastValue(underTest.scale)
+
+ underTest.statusViewTop = 100
+ underTest.topInset = 80
+
+ // Set to dozing (on AOD)
+ dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = -30,
+ scale = 0.5f,
+ )
+ assertThat(translationX).isEqualTo(20)
+ // -20 instead of -30, due to inset of 80
+ assertThat(translationY).isEqualTo(-20)
+ assertThat(scale).isEqualTo(Pair(0.5f, true /* scaleClockOnly */))
+
+ // Set to the beginning of GONE->AOD transition
+ goneToAodTransitionStep.emit(TransitionStep(value = 0f))
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
+ }
+
+ @Test
fun translationAndScaleFromBurnInUseScaleOnly() =
testScope.runTest {
whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
index edcaa1d..30e4866 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/UdfpsLockscreenViewModelTest.kt
@@ -751,14 +751,10 @@
}
private suspend fun givenTransitionToLockscreenFinished() {
- transitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.AOD,
- to = KeyguardState.LOCKSCREEN,
- value = 1f,
- transitionState = TransitionState.FINISHED,
- ownerName = "givenTransitionToLockscreenFinished",
- )
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope
)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index a4c2a08..3bfdb84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -31,7 +31,6 @@
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.keyguard.TestScopeProvider
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
@@ -39,8 +38,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
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.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
@@ -52,6 +49,7 @@
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
+import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -810,8 +808,10 @@
mediaCarouselController.mediaCarousel = mediaCarousel
val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this)
- transitionRepository.sendTransitionStep(
- TransitionStep(to = KeyguardState.GONE, transitionState = TransitionState.FINISHED)
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this
)
verify(mediaCarousel).visibility = View.VISIBLE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt
index 66d2465..c289ff3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventCoordinatorTest.kt
@@ -21,7 +21,6 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.PendingDisplay
-import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State.CONNECTED
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.privacy.PrivacyItemController
import com.android.systemui.statusbar.policy.BatteryController
@@ -79,8 +78,8 @@
testScope.runTest {
systemEventCoordinator.startObserving()
- connectedDisplayInteractor.emit(CONNECTED)
- connectedDisplayInteractor.emit(CONNECTED)
+ connectedDisplayInteractor.emit()
+ connectedDisplayInteractor.emit()
verify(scheduler, times(2)).onStatusEvent(any<ConnectedDisplayEvent>())
}
@@ -90,21 +89,23 @@
testScope.runTest {
systemEventCoordinator.startObserving()
- connectedDisplayInteractor.emit(CONNECTED)
+ connectedDisplayInteractor.emit()
verify(scheduler).onStatusEvent(any<ConnectedDisplayEvent>())
systemEventCoordinator.stopObserving()
- connectedDisplayInteractor.emit(CONNECTED)
+ connectedDisplayInteractor.emit()
verifyNoMoreInteractions(scheduler)
}
class FakeConnectedDisplayInteractor : ConnectedDisplayInteractor {
- private val flow = MutableSharedFlow<ConnectedDisplayInteractor.State>()
- suspend fun emit(value: ConnectedDisplayInteractor.State) = flow.emit(value)
+ private val flow = MutableSharedFlow<Unit>()
+ suspend fun emit() = flow.emit(Unit)
override val connectedDisplayState: Flow<ConnectedDisplayInteractor.State>
+ get() = MutableSharedFlow<ConnectedDisplayInteractor.State>()
+ override val connectedDisplayAddition: Flow<Unit>
get() = flow
override val pendingDisplay: Flow<PendingDisplay?>
get() = MutableSharedFlow<PendingDisplay>()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index 6c1f537..2ee016b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -28,8 +28,6 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
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.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
@@ -51,8 +49,6 @@
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -66,6 +62,8 @@
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import java.util.function.Consumer
+import kotlin.time.Duration.Companion.seconds
import org.mockito.Mockito.`when` as whenever
@SmallTest
@@ -131,8 +129,10 @@
collectionListener.onEntryAdded(fakeEntry)
// WHEN: The device transitions to AOD
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(to = KeyguardState.AOD, transitionState = TransitionState.STARTED),
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -147,8 +147,10 @@
keyguardRepository.setKeyguardShowing(false)
whenever(statusBarStateController.isExpanded).thenReturn(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
// WHEN: A notification is posted
@@ -161,8 +163,10 @@
// WHEN: The keyguard is now showing
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -171,8 +175,10 @@
// WHEN: The keyguard goes away
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -337,8 +343,10 @@
runKeyguardCoordinatorTest {
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -348,15 +356,19 @@
// WHEN: Keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.AOD)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -370,16 +382,20 @@
// GIVEN: Keyguard is showing, unseen notification is present
keyguardRepository.setKeyguardShowing(true)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
val fakeEntry = NotificationEntryBuilder().build()
collectionListener.onEntryAdded(fakeEntry)
// WHEN: Keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
// WHEN: Keyguard is shown again
@@ -397,8 +413,10 @@
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
val firstEntry = NotificationEntryBuilder().setId(1).build()
collectionListener.onEntryAdded(firstEntry)
@@ -419,15 +437,19 @@
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -445,8 +467,10 @@
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -473,15 +497,19 @@
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -496,8 +524,10 @@
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -524,15 +554,19 @@
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -547,8 +581,10 @@
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setIsDozing(false)
runKeyguardCoordinatorTest {
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
@@ -571,15 +607,19 @@
// WHEN: the keyguard is no longer showing
keyguardRepository.setKeyguardShowing(false)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.LOCKSCREEN, to = KeyguardState.GONE)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ this.testScheduler,
)
testScheduler.runCurrent()
// WHEN: Keyguard is shown again
keyguardRepository.setKeyguardShowing(true)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.GONE, to = KeyguardState.LOCKSCREEN)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this.testScheduler,
)
testScheduler.runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index 41c7071..14d188c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -370,11 +370,10 @@
scope.runTest {
val isVisible by collectLastValue(underTest.isVisible)
runCurrent()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.GONE,
- transitionState = TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OFF,
+ to = KeyguardState.GONE,
+ scope,
)
whenever(screenOffAnimController.shouldShowAodIconsWhenShade()).thenReturn(false)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
index 1bb7b61..2bad9f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
@@ -143,18 +143,44 @@
}
@Test
- fun onIconUpdated_consumerAlreadySet_nothingHappens() =
+ fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholder() =
testScope.runTest {
// GIVEN a consumer is set
- val otherConsumer: NotificationDrawableConsumer = mock()
iconManager.updateIcon(mockConsumer, supportedIcon).run()
clearInvocations(mockConsumer)
// WHEN a new consumer is set
- iconManager.updateIcon(otherConsumer, unsupportedIcon).run()
+ val newConsumer: NotificationDrawableConsumer = mock()
+ iconManager.updateIcon(newConsumer, supportedIcon).run()
- // THEN nothing happens
- verifyZeroInteractions(mockConsumer, otherConsumer)
+ // THEN the new consumer is updated
+ verify(newConsumer).setImageDrawable(drawableCaptor.capture())
+ assertIsPlaceHolder(drawableCaptor.value)
+ assertSize(drawableCaptor.value)
+ // AND nothing happens on the old consumer
+ verifyZeroInteractions(mockConsumer)
+ }
+
+ @Test
+ fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImage() =
+ testScope.runTest {
+ // GIVEN a consumer is set
+ iconManager.updateIcon(mockConsumer, supportedIcon).run()
+ // AND an icon is loaded
+ iconManager.onViewShown(true)
+ runCurrent()
+ clearInvocations(mockConsumer)
+
+ // WHEN a new consumer is set
+ val newConsumer: NotificationDrawableConsumer = mock()
+ iconManager.updateIcon(newConsumer, supportedIcon).run()
+
+ // THEN the new consumer is updated
+ verify(newConsumer).setImageDrawable(drawableCaptor.capture())
+ assertIsFullImage(drawableCaptor.value)
+ assertSize(drawableCaptor.value)
+ // AND nothing happens on the old consumer
+ verifyZeroInteractions(mockConsumer)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
index c4baa69..5549fee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -16,13 +16,17 @@
package com.android.systemui.statusbar.notification.row
+import android.annotation.DimenRes
import android.content.res.Resources
import android.os.UserHandle
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.ViewUtils
import android.view.NotificationHeaderView
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
@@ -30,20 +34,21 @@
import com.android.internal.widget.NotificationActionListLayout
import com.android.internal.widget.NotificationExpandButton
import com.android.systemui.SysuiTestCase
-import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.statusbar.notification.FeedbackIcon
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
-import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
@@ -53,89 +58,247 @@
@SmallTest
@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
class NotificationContentViewTest : SysuiTestCase() {
- private lateinit var view: NotificationContentView
+ private lateinit var row: ExpandableNotificationRow
+ private lateinit var fakeParent: ViewGroup
@Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
- private val notificationContentMargin =
- mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin)
+ private val testableResources = mContext.getOrCreateTestableResources()
+ private val contractedHeight =
+ px(com.android.systemui.res.R.dimen.min_notification_layout_height)
+ private val expandedHeight = px(com.android.systemui.res.R.dimen.notification_max_height)
+ private val notificationContentMargin = px(R.dimen.notification_content_margin)
@Before
fun setup() {
initMocks(this)
-
- mDependency.injectMockDependency(MediaOutputDialogFactory::class.java)
-
- view = spy(NotificationContentView(mContext, /* attrs= */ null))
- val row = ExpandableNotificationRow(mContext, /* attrs= */ null)
- row.entry = createMockNotificationEntry(false)
- val spyRow = spy(row)
- doReturn(10).whenever(spyRow).intrinsicHeight
-
- with(view) {
- initialize(mPeopleNotificationIdentifier, mock(), mock(), mock(), mock())
- setContainingNotification(spyRow)
- setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30)
- contractedChild = createViewWithHeight(10)
- expandedChild = createViewWithHeight(20)
- headsUpChild = createViewWithHeight(30)
- measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
- layout(0, 0, view.measuredWidth, view.measuredHeight)
- }
+ fakeParent = FrameLayout(mContext, /* attrs= */ null).also { it.visibility = View.GONE }
+ row =
+ spy(
+ ExpandableNotificationRow(mContext, /* attrs= */ null).apply {
+ entry = createMockNotificationEntry()
+ }
+ )
+ ViewUtils.attachView(fakeParent)
}
- private fun createViewWithHeight(height: Int) =
- View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+ @After
+ fun teardown() {
+ fakeParent.removeAllViews()
+ ViewUtils.detachView(fakeParent)
+ }
+
+ @Test
+ fun contractedWrapperSelected_whenShadeIsClosed_wrapperNotNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+
+ // WHEN a collapsed content is created
+ val view = createContentView(isSystemExpanded = false)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is visible, but NOT shown
+ verify(view.contractedWrapper).setVisible(true)
+ verify(view.contractedWrapper, never()).onContentShown(anyBoolean())
+ }
+
+ @Test
+ fun contractedWrapperSelected_whenShadeIsOpen_wrapperNotified() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+
+ // WHEN a collapsed content is created
+ val view = createContentView(isSystemExpanded = false)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is visible and shown
+ verify(view.contractedWrapper, Mockito.atLeastOnce()).setVisible(true)
+ verify(view.contractedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeOpens_collapsedWrapperIsSelected_wrapperNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+ // AND a collapsed content is created
+ val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.VISIBLE
+ view.onVisibilityAggregated(true)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is shown
+ verify(view.contractedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeCloses_collapsedWrapperIsShown_wrapperNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.VISIBLE
+ // AND a collapsed content is created
+ val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.GONE
+ view.onVisibilityAggregated(false)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is NOT shown
+ verify(view.contractedWrapper, times(1)).onContentShown(false)
+ }
+
+ @Test
+ fun expandedWrapperSelected_whenShadeIsClosed_wrapperNotNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+
+ // WHEN a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true)
+
+ // THEN the contractedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the contractedWrapper is visible, but NOT shown
+ verify(view.expandedWrapper, Mockito.atLeastOnce()).setVisible(true)
+ verify(view.expandedWrapper, never()).onContentShown(anyBoolean())
+ }
+
+ @Test
+ fun expandedWrapperSelected_whenShadeIsOpen_wrapperNotified() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+
+ // WHEN an system-expanded content is created
+ val view = createContentView(isSystemExpanded = true)
+
+ // THEN the expandedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the expandedWrapper is visible and shown
+ verify(view.expandedWrapper, Mockito.atLeastOnce()).setVisible(true)
+ verify(view.expandedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeOpens_expandedWrapperIsSelected_wrapperNotified() {
+ // GIVEN the shade is closed
+ fakeParent.visibility = View.GONE
+ // AND a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.VISIBLE
+ view.onVisibilityAggregated(true)
+
+ // THEN the expandedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the expandedWrapper is shown
+ verify(view.expandedWrapper, times(1)).onContentShown(true)
+ }
+
+ @Test
+ fun shadeCloses_expandedWrapperIsShown_wrapperNotified() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+ // AND a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
+
+ // WHEN the shade opens
+ fakeParent.visibility = View.GONE
+ view.onVisibilityAggregated(false)
+
+ // THEN the expandedWrapper is set
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ // AND the expandedWrapper is NOT shown
+ verify(view.expandedWrapper, times(1)).onContentShown(false)
+ }
+
+ @Test
+ fun expandCollapsedNotification_expandedWrapperShown() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+ // AND a collapsed content is created
+ val view = createContentView(isSystemExpanded = false).apply { clearInvocations() }
+
+ // WHEN we collapse the notification
+ whenever(row.intrinsicHeight).thenReturn(expandedHeight)
+ view.contentHeight = expandedHeight
+
+ // THEN the wrappers are updated
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
+ verify(view.contractedWrapper, times(1)).onContentShown(false)
+ verify(view.contractedWrapper).setVisible(false)
+ verify(view.expandedWrapper, times(1)).onContentShown(true)
+ verify(view.expandedWrapper).setVisible(true)
+ }
+
+ @Test
+ fun collapseExpandedNotification_expandedWrapperShown() {
+ // GIVEN the shade is open
+ fakeParent.visibility = View.VISIBLE
+ // AND a system-expanded content is created
+ val view = createContentView(isSystemExpanded = true).apply { clearInvocations() }
+
+ // WHEN we collapse the notification
+ whenever(row.intrinsicHeight).thenReturn(contractedHeight)
+ view.contentHeight = contractedHeight
+
+ // THEN the wrappers are updated
+ assertEquals(view.contractedWrapper, view.visibleWrapper)
+ verify(view.expandedWrapper, times(1)).onContentShown(false)
+ verify(view.expandedWrapper).setVisible(false)
+ verify(view.contractedWrapper, times(1)).onContentShown(true)
+ verify(view.contractedWrapper).setVisible(true)
+ }
@Test
fun testSetFeedbackIcon() {
// Given: contractedChild, enpandedChild, and headsUpChild being set
- val mockContracted = createMockNotificationHeaderView()
- val mockExpanded = createMockNotificationHeaderView()
- val mockHeadsUp = createMockNotificationHeaderView()
-
- with(view) {
- contractedChild = mockContracted
- expandedChild = mockExpanded
- headsUpChild = mockHeadsUp
- }
+ val view = createContentView(isSystemExpanded = false)
// When: FeedBackIcon is set
- view.setFeedbackIcon(
+ val icon =
FeedbackIcon(
R.drawable.ic_feedback_alerted,
R.string.notification_feedback_indicator_alerted
)
- )
+ view.setFeedbackIcon(icon)
- // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible
- verify(mockContracted).visibility = View.VISIBLE
- verify(mockExpanded).visibility = View.VISIBLE
- verify(mockHeadsUp).visibility = View.VISIBLE
+ // Then: contractedChild, enpandedChild, and headsUpChild is updated with the feedbackIcon
+ verify(view.contractedWrapper).setFeedbackIcon(icon)
+ verify(view.expandedWrapper).setFeedbackIcon(icon)
+ verify(view.headsUpWrapper).setFeedbackIcon(icon)
}
- private fun createMockNotificationHeaderView() =
- mock<NotificationHeaderView>().apply {
- whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this)
- whenever(this.context).thenReturn(mContext)
- }
-
@Test
fun testExpandButtonFocusIsCalled() {
val mockContractedEB = mock<NotificationExpandButton>()
- val mockContracted = createMockNotificationHeaderView(mockContractedEB)
+ val mockContracted = createMockNotificationHeaderView(contractedHeight, mockContractedEB)
val mockExpandedEB = mock<NotificationExpandButton>()
- val mockExpanded = createMockNotificationHeaderView(mockExpandedEB)
+ val mockExpanded = createMockNotificationHeaderView(expandedHeight, mockExpandedEB)
val mockHeadsUpEB = mock<NotificationExpandButton>()
- val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB)
+ val mockHeadsUp = createMockNotificationHeaderView(contractedHeight, mockHeadsUpEB)
- // Set up all 3 child forms
- view.contractedChild = mockContracted
- view.expandedChild = mockExpanded
- view.headsUpChild = mockHeadsUp
+ val view =
+ createContentView(
+ isSystemExpanded = false,
+ )
+
+ // Update all 3 child forms
+ view.apply {
+ contractedChild = mockContracted
+ expandedChild = mockExpanded
+ headsUpChild = mockHeadsUp
+
+ expandedWrapper = spy(expandedWrapper)
+ }
// This is required to call requestAccessibilityFocus()
view.setFocusOnVisibilityChange()
@@ -143,35 +306,41 @@
// The following will initialize the view and switch from not visible to expanded.
// (heads-up is actually an alternate form of contracted, hence this enters expanded state)
view.setHeadsUp(true)
+ assertEquals(view.expandedWrapper, view.visibleWrapper)
verify(mockContractedEB, never()).requestAccessibilityFocus()
verify(mockExpandedEB).requestAccessibilityFocus()
verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
}
- private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) =
- mock<NotificationHeaderView>().apply {
- whenever(this.animate()).thenReturn(mock())
- whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
- whenever(this.context).thenReturn(mContext)
- }
+ private fun createMockNotificationHeaderView(
+ height: Int,
+ mockExpandedEB: NotificationExpandButton
+ ) =
+ spy(NotificationHeaderView(mContext, /* attrs= */ null).apply { minimumHeight = height })
+ .apply {
+ whenever(this.animate()).thenReturn(mock())
+ whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+ }
@Test
fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
- val mockContracted = mock<NotificationHeaderView>()
+ val mockContracted = spy(createViewWithHeight(contractedHeight))
val mockExpandedActions = mock<NotificationActionListLayout>()
- val mockExpanded = mock<NotificationHeaderView>()
+ val mockExpanded = spy(createViewWithHeight(expandedHeight))
whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
val mockHeadsUpActions = mock<NotificationActionListLayout>()
- val mockHeadsUp = mock<NotificationHeaderView>()
+ val mockHeadsUp = spy(createViewWithHeight(contractedHeight))
whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
- with(view) {
- contractedChild = mockContracted
- expandedChild = mockExpanded
- headsUpChild = mockHeadsUp
- }
+ val view =
+ createContentView(
+ isSystemExpanded = false,
+ contractedView = mockContracted,
+ expandedView = mockExpanded,
+ headsUpView = mockHeadsUp
+ )
view.setRemoteInputVisible(true)
@@ -184,21 +353,23 @@
@Test
fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
- val mockContracted = mock<NotificationHeaderView>()
+ val mockContracted = spy(createViewWithHeight(contractedHeight))
val mockExpandedActions = mock<NotificationActionListLayout>()
- val mockExpanded = mock<NotificationHeaderView>()
+ val mockExpanded = spy(createViewWithHeight(expandedHeight))
whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
val mockHeadsUpActions = mock<NotificationActionListLayout>()
- val mockHeadsUp = mock<NotificationHeaderView>()
+ val mockHeadsUp = spy(createViewWithHeight(contractedHeight))
whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
- with(view) {
- contractedChild = mockContracted
- expandedChild = mockExpanded
- headsUpChild = mockHeadsUp
- }
+ val view =
+ createContentView(
+ isSystemExpanded = false,
+ contractedView = mockContracted,
+ expandedView = mockExpanded,
+ headsUpView = mockHeadsUp
+ )
view.setRemoteInputVisible(false)
@@ -212,7 +383,7 @@
fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
// Bubble button should not be shown for the given NotificationEntry
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -223,7 +394,9 @@
)
)
.thenReturn(actionListMarginTarget)
- view.setContainingNotification(mockContainingNotification)
+ val view = createContentView(isSystemExpanded = false)
+
+ view.setContainingNotification(mockContainingNotification) // maybe not needed
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -237,7 +410,7 @@
fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
// Bubble button should be shown for the given NotificationEntry
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -248,10 +421,12 @@
)
)
.thenReturn(actionListMarginTarget)
+ val view = createContentView(isSystemExpanded = false)
+
view.setContainingNotification(mockContainingNotification)
// Given: controller says bubbles are enabled for the user
- view.setBubblesEnabledForUser(true);
+ view.setBubblesEnabledForUser(true)
// When: call NotificationContentView.setExpandedChild() to set the expandedChild
view.expandedChild = mockExpandedChild
@@ -263,7 +438,7 @@
@Test
fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -274,13 +449,15 @@
)
)
.thenReturn(actionListMarginTarget)
+ val view = createContentView(isSystemExpanded = false)
+
view.setContainingNotification(mockContainingNotification)
view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should not show bubble button
- view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false))
+ view.onNotificationUpdated(createMockNotificationEntry())
// Then: bottom margin of actionListMarginTarget should not change, still be 20
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
@@ -289,7 +466,7 @@
@Test
fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
// Given: bottom margin of actionListMarginTarget is notificationContentMargin
- val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+ val mockNotificationEntry = createMockNotificationEntry()
val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
val actionListMarginTarget =
spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
@@ -300,19 +477,20 @@
)
)
.thenReturn(actionListMarginTarget)
+ val view = createContentView(isSystemExpanded = false, expandedView = mockExpandedChild)
+
view.setContainingNotification(mockContainingNotification)
- view.expandedChild = mockExpandedChild
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
// When: call NotificationContentView.onNotificationUpdated() to update the
// NotificationEntry, which should show bubble button
- view.onNotificationUpdated(createMockNotificationEntry(true))
+ view.onNotificationUpdated(createMockNotificationEntry(/*true*/ ))
// Then: no bubble yet
assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
// Given: controller says bubbles are enabled for the user
- view.setBubblesEnabledForUser(true);
+ view.setBubblesEnabledForUser(true)
// Then: bottom margin of actionListMarginTarget should not change, still be 20
assertEquals(0, getMarginBottom(actionListMarginTarget))
@@ -321,81 +499,63 @@
@Test
fun onSetAnimationRunning() {
// Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set
- val mockContracted = mock<NotificationViewWrapper>()
- val mockExpanded = mock<NotificationViewWrapper>()
- val mockHeadsUp = mock<NotificationViewWrapper>()
-
- view.setContractedWrapper(mockContracted)
- view.setExpandedWrapper(mockExpanded)
- view.setHeadsUpWrapper(mockHeadsUp)
+ val view = createContentView(isSystemExpanded = false)
// When: we set content animation running.
assertTrue(view.setContentAnimationRunning(true))
// Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
// called on them.
- verify(mockContracted, times(1)).setAnimationsRunning(true)
- verify(mockExpanded, times(1)).setAnimationsRunning(true)
- verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+ verify(view.contractedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.expandedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.headsUpWrapper, times(1)).setAnimationsRunning(true)
// When: we set content animation running true _again_.
assertFalse(view.setContentAnimationRunning(true))
// Then: the children should not have setAnimationRunning called on them again.
// Verify counts number of calls so far on the object, so these still register as 1.
- verify(mockContracted, times(1)).setAnimationsRunning(true)
- verify(mockExpanded, times(1)).setAnimationsRunning(true)
- verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
+ verify(view.contractedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.expandedWrapper, times(1)).setAnimationsRunning(true)
+ verify(view.headsUpWrapper, times(1)).setAnimationsRunning(true)
}
@Test
fun onSetAnimationStopped() {
// Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
- val mockContracted = mock<NotificationViewWrapper>()
- val mockExpanded = mock<NotificationViewWrapper>()
- val mockHeadsUp = mock<NotificationViewWrapper>()
-
- view.setContractedWrapper(mockContracted)
- view.setExpandedWrapper(mockExpanded)
- view.setHeadsUpWrapper(mockHeadsUp)
+ val view = createContentView(isSystemExpanded = false)
// When: we set content animation running.
assertTrue(view.setContentAnimationRunning(true))
// Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
// called on them.
- verify(mockContracted).setAnimationsRunning(true)
- verify(mockExpanded).setAnimationsRunning(true)
- verify(mockHeadsUp).setAnimationsRunning(true)
+ verify(view.contractedWrapper).setAnimationsRunning(true)
+ verify(view.expandedWrapper).setAnimationsRunning(true)
+ verify(view.headsUpWrapper).setAnimationsRunning(true)
// When: we set content animation running false, the state changes, so the function
// returns true.
assertTrue(view.setContentAnimationRunning(false))
// Then: the children have their animations stopped.
- verify(mockContracted).setAnimationsRunning(false)
- verify(mockExpanded).setAnimationsRunning(false)
- verify(mockHeadsUp).setAnimationsRunning(false)
+ verify(view.contractedWrapper).setAnimationsRunning(false)
+ verify(view.expandedWrapper).setAnimationsRunning(false)
+ verify(view.headsUpWrapper).setAnimationsRunning(false)
}
@Test
fun onSetAnimationInitStopped() {
// Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
- val mockContracted = mock<NotificationViewWrapper>()
- val mockExpanded = mock<NotificationViewWrapper>()
- val mockHeadsUp = mock<NotificationViewWrapper>()
-
- view.setContractedWrapper(mockContracted)
- view.setExpandedWrapper(mockExpanded)
- view.setHeadsUpWrapper(mockHeadsUp)
+ val view = createContentView(isSystemExpanded = false)
// When: we try to stop the animations before they've been started.
assertFalse(view.setContentAnimationRunning(false))
// Then: the children should not have setAnimationRunning called on them again.
- verify(mockContracted, never()).setAnimationsRunning(false)
- verify(mockExpanded, never()).setAnimationsRunning(false)
- verify(mockHeadsUp, never()).setAnimationsRunning(false)
+ verify(view.contractedWrapper, never()).setAnimationsRunning(false)
+ verify(view.expandedWrapper, never()).setAnimationsRunning(false)
+ verify(view.headsUpWrapper, never()).setAnimationsRunning(false)
}
private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
@@ -405,7 +565,7 @@
whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
}
- private fun createMockNotificationEntry(showButton: Boolean) =
+ private fun createMockNotificationEntry() =
mock<NotificationEntry>().apply {
whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
.thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
@@ -426,10 +586,9 @@
}
private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
- mock<ExpandableNotificationRow>().apply {
+ spy(createViewWithHeight(expandedHeight)).apply {
whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
- whenever(this.entry).thenReturn(notificationEntry)
whenever(this.context).thenReturn(mContext)
val resourcesMock: Resources = mock()
@@ -437,6 +596,56 @@
whenever(this.resources).thenReturn(resourcesMock)
}
+ private fun createContentView(
+ isSystemExpanded: Boolean,
+ contractedView: View = createViewWithHeight(contractedHeight),
+ expandedView: View = createViewWithHeight(expandedHeight),
+ headsUpView: View = createViewWithHeight(contractedHeight),
+ row: ExpandableNotificationRow = this.row
+ ): NotificationContentView {
+ val height = if (isSystemExpanded) expandedHeight else contractedHeight
+ doReturn(height).whenever(row).intrinsicHeight
+
+ return spy(NotificationContentView(mContext, /* attrs= */ null))
+ .apply {
+ initialize(mPeopleNotificationIdentifier, mock(), mock(), mock(), mock())
+ setContainingNotification(row)
+ setHeights(
+ /* smallHeight= */ contractedHeight,
+ /* headsUpMaxHeight= */ contractedHeight,
+ /* maxHeight= */ expandedHeight
+ )
+ contractedChild = contractedView
+ expandedChild = expandedView
+ headsUpChild = headsUpView
+ contractedWrapper = spy(contractedWrapper)
+ expandedWrapper = spy(expandedWrapper)
+ headsUpWrapper = spy(headsUpWrapper)
+
+ if (isSystemExpanded) {
+ contentHeight = expandedHeight
+ }
+ }
+ .also { contentView ->
+ fakeParent.addView(contentView)
+ contentView.mockRequestLayout()
+ }
+ }
+
+ private fun createViewWithHeight(height: Int) =
+ View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+
private fun getMarginBottom(layout: LinearLayout): Int =
(layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
+
+ private fun px(@DimenRes id: Int): Int = testableResources.resources.getDimensionPixelSize(id)
+}
+
+private fun NotificationContentView.mockRequestLayout() {
+ measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+ layout(0, 0, measuredWidth, measuredHeight)
+}
+
+private fun NotificationContentView.clearInvocations() {
+ Mockito.clearInvocations(contractedWrapper, expandedWrapper, headsUpWrapper)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 033c96a..4af7864 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -786,6 +786,25 @@
}
@Test
+ public void testSetOwnScrollY_clearAllInProgress_scrollYDoesNotChange() {
+ // Given: clear all is in progress, scrollY is 0
+ mAmbientState.setScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ mAmbientState.setClearAllInProgress(true);
+
+ // When: call NotificationStackScrollLayout.setOwnScrollY to set scrollY to 1
+ mStackScroller.setOwnScrollY(1);
+
+ // Then: scrollY should not change, it should still be 0
+ assertEquals(0, mAmbientState.getScrollY());
+
+ // Reset scrollY and mAmbientState.mIsClosing to avoid interfering with other tests
+ mAmbientState.setClearAllInProgress(false);
+ mStackScroller.setOwnScrollY(0);
+ assertEquals(0, mAmbientState.getScrollY());
+ }
+
+ @Test
public void onShadeFlingClosingEnd_scrollYShouldBeSetToZero() {
// Given: mAmbientState.mIsClosing is set to be true
// mIsExpanded is set to be false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 0a7dc4e..978fafe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -191,13 +191,10 @@
testScope.runTest {
val isOnLockscreen by collectLastValue(underTest.isOnLockscreen)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.GONE,
- value = 1f,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
)
assertThat(isOnLockscreen).isFalse()
@@ -212,19 +209,17 @@
)
assertThat(isOnLockscreen).isTrue()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.LOCKSCREEN,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ this,
)
assertThat(isOnLockscreen).isTrue()
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.PRIMARY_BOUNCER,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ testScope,
)
assertThat(isOnLockscreen).isTrue()
}
@@ -237,11 +232,10 @@
// First on AOD
shadeRepository.setLockscreenShadeExpansion(0f)
shadeRepository.setQsExpansion(0f)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.OCCLUDED,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ testScope,
)
assertThat(isOnLockscreenWithoutShade).isFalse()
@@ -271,12 +265,13 @@
testScope.runTest {
val position by collectLastValue(underTest.position)
- // Start on lockscreen
- showLockscreen()
-
// When not in split shade
overrideResource(R.bool.config_use_split_notification_shade, false)
configurationRepository.onAnyConfigurationChange()
+ runCurrent()
+
+ // Start on lockscreen
+ showLockscreen()
keyguardInteractor.sharedNotificationContainerPosition.value =
SharedNotificationContainerPosition(top = 1f, bottom = 2f)
@@ -290,12 +285,13 @@
testScope.runTest {
val position by collectLastValue(underTest.position)
- // Start on lockscreen
- showLockscreen()
-
// When in split shade
overrideResource(R.bool.config_use_split_notification_shade, true)
configurationRepository.onAnyConfigurationChange()
+ runCurrent()
+
+ // Start on lockscreen
+ showLockscreen()
keyguardInteractor.sharedNotificationContainerPosition.value =
SharedNotificationContainerPosition(top = 1f, bottom = 2f)
@@ -318,7 +314,26 @@
sharedNotificationContainerInteractor.setTopPosition(10f)
assertThat(position)
- .isEqualTo(SharedNotificationContainerPosition(top = 10f, bottom = 0f))
+ .isEqualTo(
+ SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = true)
+ )
+ }
+
+ @Test
+ fun positionOnQS() =
+ testScope.runTest {
+ val position by collectLastValue(underTest.position)
+
+ // Start on lockscreen with shade expanded
+ showLockscreenWithQSExpanded()
+
+ // When not in split shade
+ sharedNotificationContainerInteractor.setTopPosition(10f)
+
+ assertThat(position)
+ .isEqualTo(
+ SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = false)
+ )
}
@Test
@@ -372,22 +387,32 @@
assertThat(maxNotifications).isEqualTo(-1)
}
- private suspend fun showLockscreen() {
+ private suspend fun TestScope.showLockscreen() {
shadeRepository.setLockscreenShadeExpansion(0f)
shadeRepository.setQsExpansion(0f)
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- to = KeyguardState.LOCKSCREEN,
- transitionState = TransitionState.FINISHED
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this,
)
}
- private suspend fun showLockscreenWithShadeExpanded() {
+ private suspend fun TestScope.showLockscreenWithShadeExpanded() {
shadeRepository.setLockscreenShadeExpansion(1f)
shadeRepository.setQsExpansion(0f)
keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ this,
+ )
+ }
+
+ private suspend fun showLockscreenWithQSExpanded() {
+ shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeRepository.setQsExpansion(1f)
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
keyguardTransitionRepository.sendTransitionStep(
TransitionStep(
to = KeyguardState.LOCKSCREEN,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index 71c27de..da6c28a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -317,6 +317,8 @@
suspend fun emit(value: State) = flow.emit(value)
override val connectedDisplayState: Flow<State>
get() = flow
+ override val connectedDisplayAddition: Flow<Unit>
+ get() = TODO("Not yet implemented")
override val pendingDisplay: Flow<PendingDisplay?>
get() = TODO("Not yet implemented")
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 842d548..688f739 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -99,13 +99,10 @@
testScope.runTest {
val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
- keyguardTransitionRepository.sendTransitionStep(
- TransitionStep(
- KeyguardState.LOCKSCREEN,
- KeyguardState.OCCLUDED,
- value = 0f,
- TransitionState.FINISHED,
- )
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OCCLUDED,
+ this.testScheduler,
)
assertThat(underTest.isTransitioningFromLockscreenToOccluded.value).isFalse()
@@ -312,7 +309,10 @@
KeyguardState.DREAMING,
value = 1.0f,
TransitionState.FINISHED,
- )
+ ),
+ // We're intentionally not sending STARTED to validate that FINISHED steps are
+ // ignored.
+ validateStep = false,
)
assertThat(emissions).isEmpty()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
index ef02bdd..44286b7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
@@ -16,6 +16,16 @@
package com.android.systemui.deviceentry.data
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeTrustRepositoryModule
import dagger.Module
-@Module(includes = [FakeDeviceEntryRepositoryModule::class]) object FakeDeviceEntryDataLayerModule
+@Module(
+ includes =
+ [
+ FakeDeviceEntryRepositoryModule::class,
+ FakeTrustRepositoryModule::class,
+ FakeDeviceEntryFaceAuthRepositoryModule::class,
+ ]
+)
+object FakeDeviceEntryDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
index 5fd0b4f..d8098b7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt
@@ -47,6 +47,10 @@
private val flow = MutableSharedFlow<Set<Display>>(replay = 1)
private val pendingDisplayFlow =
MutableSharedFlow<DisplayRepository.PendingDisplay?>(replay = 1)
+ private val displayAdditionEventFlow = MutableSharedFlow<Display?>(replay = 1)
+
+ /** Emits [value] as [displayAdditionEvent] flow value. */
+ suspend fun emit(value: Display?) = displayAdditionEventFlow.emit(value)
/** Emits [value] as [displays] flow value. */
suspend fun emit(value: Set<Display>) = flow.emit(value)
@@ -60,6 +64,9 @@
override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?>
get() = pendingDisplayFlow
+ override val displayAdditionEvent: Flow<Display?>
+ get() = displayAdditionEventFlow
+
private val _displayChangeEvent = MutableSharedFlow<Int>(replay = 1)
override val displayChangeEvent: Flow<Int> = _displayChangeEvent
suspend fun emitDisplayChangeEvent(displayId: Int) = _displayChangeEvent.emit(displayId)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
index 6710072..abf72af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard.data
import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule
import dagger.Module
@@ -25,7 +24,6 @@
includes =
[
FakeCommandQueueModule::class,
- FakeDeviceEntryFaceAuthRepositoryModule::class,
FakeKeyguardRepositoryModule::class,
FakeKeyguardTransitionRepositoryModule::class,
]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
index 71e2bc1..b90ad8c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt
@@ -19,6 +19,7 @@
import android.annotation.FloatRange
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -26,9 +27,13 @@
import dagger.Module
import java.util.UUID
import javax.inject.Inject
+import junit.framework.Assert.fail
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
/** Fake implementation of [KeyguardTransitionRepository] */
@SysUISingleton
@@ -38,7 +43,111 @@
MutableSharedFlow<TransitionStep>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val transitions: SharedFlow<TransitionStep> = _transitions
- suspend fun sendTransitionStep(step: TransitionStep) {
+ init {
+ _transitions.tryEmit(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ )
+ )
+
+ _transitions.tryEmit(
+ TransitionStep(
+ transitionState = TransitionState.FINISHED,
+ from = KeyguardState.OFF,
+ to = KeyguardState.LOCKSCREEN,
+ )
+ )
+ }
+
+ /**
+ * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
+ * [runCurrent] after each step.
+ */
+ suspend fun sendTransitionSteps(
+ from: KeyguardState,
+ to: KeyguardState,
+ testScope: TestScope,
+ ) {
+ sendTransitionSteps(from, to, testScope.testScheduler)
+ }
+
+ /**
+ * Sends STARTED, RUNNING, and FINISHED TransitionSteps between [from] and [to], calling
+ * [runCurrent] after each step.
+ */
+ suspend fun sendTransitionSteps(
+ from: KeyguardState,
+ to: KeyguardState,
+ testScheduler: TestCoroutineScheduler,
+ ) {
+ sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = from,
+ to = to,
+ value = 0f,
+ )
+ )
+ testScheduler.runCurrent()
+
+ sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.RUNNING,
+ from = from,
+ to = to,
+ value = 0.5f
+ )
+ )
+ testScheduler.runCurrent()
+
+ sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.FINISHED,
+ from = from,
+ to = to,
+ value = 1f,
+ )
+ )
+ testScheduler.runCurrent()
+ }
+
+ /**
+ * Directly emits the provided TransitionStep, which can be useful in tests for testing behavior
+ * during specific phases of a transition (such as asserting values while a transition has
+ * STARTED but not FINISHED).
+ *
+ * WARNING: You can get the transition repository into undefined states using this method - for
+ * example, you could send a FINISHED step to LOCKSCREEN having never sent a STARTED step. This
+ * can get flows that combine startedStep/finishedStep into a bad state.
+ *
+ * If you are just trying to get the transition repository FINISHED in a certain state, use
+ * [sendTransitionSteps] - this will send STARTED, RUNNING, and FINISHED steps for you which
+ * ensures that [KeyguardTransitionInteractor] flows will be in the correct state.
+ *
+ * If you're testing something involving transitions themselves and are sure you want to send
+ * only a FINISHED step, override [validateStep].
+ */
+ suspend fun sendTransitionStep(step: TransitionStep, validateStep: Boolean = true) {
+ _transitions.replayCache.getOrNull(0)?.let { lastStep ->
+ if (
+ validateStep &&
+ step.transitionState == TransitionState.FINISHED &&
+ !(lastStep.transitionState == TransitionState.STARTED ||
+ lastStep.transitionState == TransitionState.RUNNING)
+ ) {
+ fail(
+ "Attempted to send a FINISHED TransitionStep without a prior " +
+ "STARTED/RUNNING step. This leaves the FakeKeyguardTransitionRepository " +
+ "in an undefined state and should not be done. Pass " +
+ "allowInvalidStep=true to sendTransitionStep if you are trying to test " +
+ "this specific and" +
+ "incorrect state."
+ )
+ }
+ }
+
_transitions.emit(step)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
index 482126d..cd83c2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
@@ -18,13 +18,18 @@
package com.android.systemui.keyguard.data.repository
import com.android.keyguard.TrustGrantFlags
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.TrustModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-class FakeTrustRepository : TrustRepository {
+@SysUISingleton
+class FakeTrustRepository @Inject constructor() : TrustRepository {
private val _isTrustUsuallyManaged = MutableStateFlow(false)
override val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
get() = _isTrustUsuallyManaged
@@ -63,3 +68,8 @@
_isTrustUsuallyManaged.value = value
}
}
+
+@Module
+interface FakeTrustRepositoryModule {
+ @Binds fun bindFake(fake: FakeTrustRepository): TrustRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 17384351..bdddc04 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -42,9 +42,13 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
@@ -155,12 +159,16 @@
repository: DeviceEntryRepository = deviceEntryRepository,
authenticationInteractor: AuthenticationInteractor,
sceneInteractor: SceneInteractor,
+ faceAuthRepository: DeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
+ trustRepository: TrustRepository = FakeTrustRepository(),
): DeviceEntryInteractor {
return DeviceEntryInteractor(
applicationScope = applicationScope(),
repository = repository,
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
+ deviceEntryFaceAuthRepository = faceAuthRepository,
+ trustRepository = trustRepository,
)
}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 75ecdb7..a19920f 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -10,6 +10,13 @@
}
flag {
+ name: "cleanup_a11y_overlays"
+ namespace: "accessibility"
+ description: "Removes all attached accessibility overlays when a service is removed."
+ bug: "271490102"
+}
+
+flag {
name: "deprecate_package_list_observer"
namespace: "accessibility"
description: "Stops using the deprecated PackageListObserver."
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index fa73cff..6d82b74 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -224,6 +224,9 @@
final SparseArray<IBinder> mOverlayWindowTokens = new SparseArray();
+ // All the embedded accessibility overlays that have been added by this service.
+ private List<SurfaceControl> mOverlays = new ArrayList<>();
+
/** The timestamp of requesting to take screenshot in milliseconds */
private long mRequestTakeScreenshotTimestampMs;
/**
@@ -1554,6 +1557,9 @@
final int displayId = displays[i].getDisplayId();
onDisplayRemoved(displayId);
}
+ if (Flags.cleanupA11yOverlays()) {
+ detachAllOverlays();
+ }
}
/**
@@ -2677,6 +2683,7 @@
try {
mSystemSupport.attachAccessibilityOverlayToDisplay(
interactionId, displayId, sc, callback);
+ mOverlays.add(sc);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -2707,10 +2714,23 @@
connection
.getRemote()
.attachAccessibilityOverlayToWindow(sc, interactionId, callback);
+ mOverlays.add(sc);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
+
+ protected void detachAllOverlays() {
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ for (SurfaceControl sc : mOverlays) {
+ if (sc.isValid()) {
+ t.reparent(sc, null);
+ }
+ }
+ t.apply();
+ t.close();
+ mOverlays.clear();
+ }
}
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 852e36d..8c728f1 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -118,6 +118,7 @@
private final Object mGenericWindowPolicyControllerLock = new Object();
@Nullable private final ActivityBlockedCallback mActivityBlockedCallback;
private int mDisplayId = Display.INVALID_DISPLAY;
+ private boolean mIsMirrorDisplay = false;
@NonNull
@GuardedBy("mGenericWindowPolicyControllerLock")
@@ -203,8 +204,9 @@
/**
* Expected to be called once this object is associated with a newly created display.
*/
- public void setDisplayId(int displayId) {
+ void setDisplayId(int displayId, boolean isMirrorDisplay) {
mDisplayId = displayId;
+ mIsMirrorDisplay = isMirrorDisplay;
}
/**
@@ -256,9 +258,7 @@
@Nullable Intent intent, @WindowConfiguration.WindowingMode int windowingMode,
int launchingFromDisplayId, boolean isNewTask) {
if (!canContainActivity(activityInfo, windowingMode, launchingFromDisplayId, isNewTask)) {
- if (mActivityBlockedCallback != null) {
- mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo);
- }
+ notifyActivityBlocked(activityInfo);
return false;
}
if (mIntentListenerCallback != null && intent != null
@@ -273,6 +273,11 @@
public boolean canContainActivity(@NonNull ActivityInfo activityInfo,
@WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId,
boolean isNewTask) {
+ // Mirror displays cannot contain activities.
+ if (mIsMirrorDisplay) {
+ Slog.d(TAG, "Mirror virtual displays cannot contain activities.");
+ return false;
+ }
if (!isWindowingModeSupported(windowingMode)) {
Slog.d(TAG, "Virtual device doesn't support windowing mode " + windowingMode);
return false;
@@ -344,9 +349,7 @@
// TODO(b/201712607): Add checks for the apps that use SurfaceView#setSecure.
if ((windowFlags & FLAG_SECURE) != 0
|| (systemWindowFlags & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != 0) {
- if (mActivityBlockedCallback != null) {
- mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo);
- }
+ notifyActivityBlocked(activityInfo);
return false;
}
}
@@ -428,6 +431,14 @@
&& mDisplayCategories.contains(activityInfo.requiredDisplayCategory);
}
+ private void notifyActivityBlocked(ActivityInfo activityInfo) {
+ // Don't trigger activity blocked callback for mirror displays, because we can't show
+ // any activity or presentation on it anyway.
+ if (!mIsMirrorDisplay && mActivityBlockedCallback != null) {
+ mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo);
+ }
+ }
+
private static boolean isAllowedByPolicy(boolean allowedByDefault,
Set<ComponentName> exemptions, ComponentName component) {
// Either allowed and the exemptions do not contain the component,
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 4298c07..a036383 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -167,7 +167,8 @@
private final VirtualDeviceLog mVirtualDeviceLog;
private final String mOwnerPackageName;
private final int mDeviceId;
- private @Nullable final String mPersistentDeviceId;
+ @Nullable
+ private final String mPersistentDeviceId;
// Thou shall not hold the mVirtualDeviceLock over the mInputController calls.
// Holding the lock can lead to lock inversion with GlobalWindowManagerLock.
// 1. After display is created the window manager calls into VDM during construction
@@ -191,6 +192,7 @@
private final IVirtualDeviceActivityListener mActivityListener;
private final IVirtualDeviceSoundEffectListener mSoundEffectListener;
private final DisplayManagerGlobal mDisplayManager;
+ private final DisplayManagerInternal mDisplayManagerInternal;
@GuardedBy("mVirtualDeviceLock")
private final Map<IBinder, IntentFilter> mIntentInterceptors = new ArrayMap<>();
@NonNull
@@ -313,6 +315,7 @@
mParams = params;
mDevicePolicies = params.getDevicePolicies();
mDisplayManager = displayManager;
+ mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
if (inputController == null) {
mInputController = new InputController(
context.getMainThreadHandler(),
@@ -323,7 +326,9 @@
mSensorController = new SensorController(this, mDeviceId,
mParams.getVirtualSensorCallback(), mParams.getVirtualSensorConfigs());
mCameraAccessController = cameraAccessController;
- mCameraAccessController.startObservingIfNeeded();
+ if (mCameraAccessController != null) {
+ mCameraAccessController.startObservingIfNeeded();
+ }
if (!Flags.streamPermissions()) {
mPermissionDialogComponent = getPermissionDialogComponent();
} else {
@@ -563,7 +568,9 @@
}
mAppToken.unlinkToDeath(this, 0);
- mCameraAccessController.stopObservingIfNeeded();
+ if (mCameraAccessController != null) {
+ mCameraAccessController.stopObservingIfNeeded();
+ }
mInputController.close();
mSensorController.close();
@@ -583,7 +590,9 @@
@Override
@RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA)
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {
- mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+ if (mCameraAccessController != null) {
+ mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+ }
mRunningAppsChangedCallback.accept(runningUids);
}
@@ -971,8 +980,8 @@
@NonNull Set<String> displayCategories) {
final boolean activityLaunchAllowedByDefault =
Flags.dynamicPolicy()
- ? getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT
- : mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED;
+ ? getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT
+ : mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED;
final boolean crossTaskNavigationAllowedByDefault =
mParams.getDefaultNavigationPolicy() == NAVIGATION_POLICY_DEFAULT_ALLOWED;
final boolean showTasksInHostDeviceRecents =
@@ -987,7 +996,7 @@
activityLaunchAllowedByDefault,
mActivityPolicyExemptions,
crossTaskNavigationAllowedByDefault,
- /*crossTaskNavigationExemptions=*/crossTaskNavigationAllowedByDefault
+ /* crossTaskNavigationExemptions= */crossTaskNavigationAllowedByDefault
? mParams.getBlockedCrossTaskNavigations()
: mParams.getAllowedCrossTaskNavigations(),
mPermissionDialogComponent,
@@ -1016,12 +1025,12 @@
synchronized (mVirtualDeviceLock) {
gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
}
- DisplayManagerInternal displayManager = LocalServices.getService(
- DisplayManagerInternal.class);
int displayId;
- displayId = displayManager.createVirtualDisplay(virtualDisplayConfig, callback,
+ displayId = mDisplayManagerInternal.createVirtualDisplay(virtualDisplayConfig, callback,
this, gwpc, packageName);
- gwpc.setDisplayId(displayId);
+ gwpc.setDisplayId(displayId, /* isMirrorDisplay= */ Flags.interactiveScreenMirror()
+ && mDisplayManagerInternal.getDisplayIdToMirror(displayId)
+ != Display.INVALID_DISPLAY);
boolean showPointer;
synchronized (mVirtualDeviceLock) {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 0604510..959f69e 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -42,6 +42,7 @@
import android.content.AttributionSource;
import android.content.Context;
import android.content.Intent;
+import android.hardware.display.DisplayManagerInternal;
import android.hardware.display.IVirtualDisplayCallback;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Binder;
@@ -66,6 +67,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.modules.expresslog.Counter;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.companion.virtual.VirtualDeviceImpl.PendingTrampoline;
import com.android.server.wm.ActivityInterceptorCallback;
@@ -185,6 +187,9 @@
}
CameraAccessController getCameraAccessController(UserHandle userHandle) {
+ if (Flags.streamCamera()) {
+ return null;
+ }
int userId = userHandle.getIdentifier();
synchronized (mVirtualDeviceManagerLock) {
for (int i = 0; i < mVirtualDevices.size(); i++) {
@@ -545,6 +550,17 @@
}
}
+ @Override // Binder call
+ public boolean isVirtualDeviceOwnedMirrorDisplay(int displayId) {
+ if (getDeviceIdForDisplayId(displayId) == Context.DEVICE_ID_DEFAULT) {
+ return false;
+ }
+
+ DisplayManagerInternal displayManager = LocalServices.getService(
+ DisplayManagerInternal.class);
+ return displayManager.getDisplayIdToMirror(displayId) != Display.INVALID_DISPLAY;
+ }
+
@Nullable
private AssociationInfo getAssociationInfo(String packageName, int associationId) {
final UserHandle userHandle = getCallingUserHandle();
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3985a0e..7dbf61b 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -96,26 +96,11 @@
out: ["com/android/server/location/contexthub/ContextHubStatsLog.java"],
}
-/*
- * This module is used to refer aconfig flag libraries that are
- * added to the framework via static_libs.
- * These libraries are referred here via libs prevent duplication of classes in both
- * the framework and the system server.
-*/
-java_defaults {
- name: "shared-framework-aconfig-libs",
- libs: [
- "display_flags_lib",
- "camera_platform_flags_core_java_lib",
- ],
-}
-
java_library_static {
name: "services.core.unboosted",
defaults: [
"platform_service_defaults",
"android.hardware.power-java_static",
- "shared-framework-aconfig-libs",
],
srcs: [
":android.hardware.biometrics.face-V3-java-source",
@@ -174,7 +159,7 @@
"android.hardware.boot-V1.2-java", // HIDL
"android.hardware.boot-V1-java", // AIDL
"android.hardware.broadcastradio-V2.0-java", // HIDL
- "android.hardware.broadcastradio-V1-java", // AIDL
+ "android.hardware.broadcastradio-V2-java", // AIDL
"android.hardware.health-V1.0-java", // HIDL
"android.hardware.health-V2.0-java", // HIDL
"android.hardware.health-V2.1-java", // HIDL
@@ -207,12 +192,12 @@
"com.android.sysprop.watchdog",
"ImmutabilityAnnotation",
"securebox",
- "android.content.pm.flags-aconfig-java",
"apache-commons-math",
"backstage_power_flags_lib",
"notification_flags_lib",
"biometrics_flags_lib",
"am_flags_lib",
+ "com_android_wm_shell_flags_lib",
],
javac_shard_size: 50,
javacflags: [
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e88d0c6..ae79f19 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20305,7 +20305,7 @@
final long token = Binder.clearCallingIdentity();
try {
- return mOomAdjuster.mCachedAppOptimizer.isFreezerSupported();
+ return CachedAppOptimizer.isFreezerSupported();
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -20433,4 +20433,21 @@
return index >= 0 && !mMediaProjectionTokenMap.valueAt(index).isEmpty();
}
}
+
+ /**
+ * Deal with binder transactions to frozen apps.
+ *
+ * @param debugPid The binder transaction sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ @Override
+ public void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err) {
+ final ProcessRecord app;
+ synchronized (mPidsSelfLocked) {
+ app = mPidsSelfLocked.get(debugPid);
+ }
+ mOomAdjuster.mCachedAppOptimizer.binderError(debugPid, app, code, flags, err);
+ }
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 9e48b0a..f9fc4d4 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -191,6 +191,7 @@
.replaceWith("?");
private static final int MAX_LOW_POWER_STATS_SIZE = 32768;
private static final int POWER_STATS_QUERY_TIMEOUT_MILLIS = 2000;
+ private static final String DEVICE_CONFIG_NAMESPACE = "backstage_power";
private static final String MIN_CONSUMED_POWER_THRESHOLD_KEY = "min_consumed_power_threshold";
private static final String EMPTY = "Empty";
@@ -906,7 +907,7 @@
case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET:
@SuppressLint("MissingPermission")
final double minConsumedPowerThreshold =
- DeviceConfig.getFloat(DeviceConfig.NAMESPACE_BATTERY_STATS,
+ DeviceConfig.getFloat(DEVICE_CONFIG_NAMESPACE,
MIN_CONSUMED_POWER_THRESHOLD_KEY, 0);
final BatteryUsageStatsQuery querySinceReset =
new BatteryUsageStatsQuery.Builder()
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 6005b64..68af626 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -52,6 +52,8 @@
import android.app.ActivityManagerInternal.OomAdjReason;
import android.app.ActivityThread;
import android.app.ApplicationExitInfo;
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
import android.app.IApplicationThread;
import android.database.ContentObserver;
import android.net.Uri;
@@ -67,6 +69,7 @@
import android.provider.DeviceConfig.Properties;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.EventLog;
import android.util.IntArray;
import android.util.Pair;
@@ -75,6 +78,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BinderfsStatsReader;
import com.android.internal.os.ProcLocksReader;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.ServiceThread;
@@ -131,6 +135,12 @@
"freeze_binder_offset";
@VisibleForTesting static final String KEY_FREEZER_BINDER_THRESHOLD =
"freeze_binder_threshold";
+ @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_ENABLED =
+ "freeze_binder_callback_enabled";
+ @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_THROTTLE =
+ "freeze_binder_callback_throttle";
+ @VisibleForTesting static final String KEY_FREEZER_BINDER_ASYNC_THRESHOLD =
+ "freeze_binder_async_threshold";
static final int UNFREEZE_REASON_NONE =
FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_NONE;
@@ -270,6 +280,9 @@
@VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4;
@VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500;
@VisibleForTesting static final long DEFAULT_FREEZER_BINDER_THRESHOLD = 1_000;
+ @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED = true;
+ @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE = 10_000L;
+ @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD = 1_024;
@VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
Settings.Global.CACHED_APPS_FREEZER_ENABLED);
@@ -312,6 +325,7 @@
static final int COMPACT_NATIVE_MSG = 5;
static final int UID_FROZEN_STATE_CHANGED_MSG = 6;
static final int DEADLOCK_WATCHDOG_MSG = 7;
+ static final int BINDER_ERROR_MSG = 8;
// When free swap falls below this percentage threshold any full (file + anon)
// compactions will be downgraded to file only compactions to reduce pressure
@@ -408,7 +422,10 @@
} else if (KEY_FREEZER_BINDER_ENABLED.equals(name)
|| KEY_FREEZER_BINDER_DIVISOR.equals(name)
|| KEY_FREEZER_BINDER_THRESHOLD.equals(name)
- || KEY_FREEZER_BINDER_OFFSET.equals(name)) {
+ || KEY_FREEZER_BINDER_OFFSET.equals(name)
+ || KEY_FREEZER_BINDER_CALLBACK_ENABLED.equals(name)
+ || KEY_FREEZER_BINDER_CALLBACK_THROTTLE.equals(name)
+ || KEY_FREEZER_BINDER_ASYNC_THRESHOLD.equals(name)) {
updateFreezerBinderState();
}
}
@@ -480,7 +497,15 @@
@VisibleForTesting volatile int mFreezerBinderOffset = DEFAULT_FREEZER_BINDER_OFFSET;
@GuardedBy("mPhenotypeFlagLock")
@VisibleForTesting volatile long mFreezerBinderThreshold = DEFAULT_FREEZER_BINDER_THRESHOLD;
-
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting volatile boolean mFreezerBinderCallbackEnabled =
+ DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED;
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting volatile long mFreezerBinderCallbackThrottle =
+ DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE;
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting volatile int mFreezerBinderAsyncThreshold =
+ DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD;
// Handler on which compaction runs.
@VisibleForTesting
@@ -488,6 +513,7 @@
private Handler mFreezeHandler;
@GuardedBy("mProcLock")
private boolean mFreezerOverride = false;
+ private long mFreezerBinderCallbackLast = -1;
@VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
@VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
@@ -790,6 +816,12 @@
pw.println(" " + KEY_FREEZER_BINDER_THRESHOLD + "=" + mFreezerBinderThreshold);
pw.println(" " + KEY_FREEZER_BINDER_DIVISOR + "=" + mFreezerBinderDivisor);
pw.println(" " + KEY_FREEZER_BINDER_OFFSET + "=" + mFreezerBinderOffset);
+ pw.println(" " + KEY_FREEZER_BINDER_CALLBACK_ENABLED + "="
+ + mFreezerBinderCallbackEnabled);
+ pw.println(" " + KEY_FREEZER_BINDER_CALLBACK_THROTTLE + "="
+ + mFreezerBinderCallbackThrottle);
+ pw.println(" " + KEY_FREEZER_BINDER_ASYNC_THRESHOLD + "="
+ + mFreezerBinderAsyncThreshold);
synchronized (mProcLock) {
int size = mFrozenProcesses.size();
pw.println(" Apps frozen: " + size);
@@ -1309,10 +1341,22 @@
mFreezerBinderThreshold = DeviceConfig.getLong(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
KEY_FREEZER_BINDER_THRESHOLD, DEFAULT_FREEZER_BINDER_THRESHOLD);
+ mFreezerBinderCallbackEnabled = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ KEY_FREEZER_BINDER_CALLBACK_ENABLED, DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED);
+ mFreezerBinderCallbackThrottle = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ KEY_FREEZER_BINDER_CALLBACK_THROTTLE, DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE);
+ mFreezerBinderAsyncThreshold = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ KEY_FREEZER_BINDER_ASYNC_THRESHOLD, DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD);
Slog.d(TAG_AM, "Freezer binder state set to enabled=" + mFreezerBinderEnabled
+ ", divisor=" + mFreezerBinderDivisor
+ ", offset=" + mFreezerBinderOffset
- + ", threshold=" + mFreezerBinderThreshold);
+ + ", threshold=" + mFreezerBinderThreshold
+ + ", callback enabled=" + mFreezerBinderCallbackEnabled
+ + ", callback throttle=" + mFreezerBinderCallbackThrottle
+ + ", async threshold=" + mFreezerBinderAsyncThreshold);
}
private boolean parseProcStateThrottle(String procStateThrottleString) {
@@ -2182,6 +2226,21 @@
Slog.w(TAG_AM, "Unable to check file locks");
}
} break;
+ case BINDER_ERROR_MSG: {
+ IntArray pids = new IntArray();
+ // Copy the frozen pids to a local array to release mProcLock ASAP
+ synchronized (mProcLock) {
+ int size = mFrozenProcesses.size();
+ for (int i = 0; i < size; i++) {
+ pids.add(mFrozenProcesses.keyAt(i));
+ }
+ }
+
+ // Check binder errors to frozen processes with a local freezer lock
+ synchronized (mFreezerLock) {
+ binderErrorLocked(pids);
+ }
+ } break;
default:
return;
}
@@ -2487,4 +2546,115 @@
return UNFREEZE_REASON_NONE;
}
}
+
+ /**
+ * Kill a frozen process with a specified reason
+ */
+ public void killProcess(int pid, String reason, @Reason int reasonCode,
+ @SubReason int subReason) {
+ mAm.mHandler.post(() -> {
+ synchronized (mAm) {
+ synchronized (mProcLock) {
+ ProcessRecord proc = mFrozenProcesses.get(pid);
+ // The process might have been killed or unfrozen by others
+ if (proc != null && proc.getThread() != null && !proc.isKilledByAm()) {
+ proc.killLocked(reason, reasonCode, subReason, true);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sending binder transactions to frozen apps most likely indicates there's a bug. Log it and
+ * kill the frozen apps if they 1) receive sync binder transactions while frozen, or 2) miss
+ * async binder transactions due to kernel binder buffer running out.
+ *
+ * @param debugPid The binder transaction sender
+ * @param app The ProcessRecord of the sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ public void binderError(int debugPid, ProcessRecord app, int code, int flags, int err) {
+ Slog.w(TAG_AM, "pid " + debugPid + " " + (app == null ? "null" : app.processName)
+ + " sent binder code " + code + " with flags " + flags
+ + " to frozen apps and got error " + err);
+
+ // Do nothing if the binder error callback is not enabled.
+ // That means the frozen apps in a wrong state will be killed when they are unfrozen later.
+ if (!mFreezerBinderCallbackEnabled) {
+ return;
+ }
+
+ final long now = SystemClock.uptimeMillis();
+ if (now < mFreezerBinderCallbackLast + mFreezerBinderCallbackThrottle) {
+ Slog.d(TAG_AM, "Too many transaction errors, throttling freezer binder callback.");
+ return;
+ }
+ mFreezerBinderCallbackLast = now;
+
+ // Check all frozen processes in Freezer handler
+ mFreezeHandler.sendEmptyMessage(BINDER_ERROR_MSG);
+ }
+
+ private void binderErrorLocked(IntArray pids) {
+ // PIDs that run out of async binder buffer when being frozen
+ ArraySet<Integer> pidsAsync = (mFreezerBinderAsyncThreshold < 0) ? null : new ArraySet<>();
+
+ for (int i = 0; i < pids.size(); i++) {
+ int current = pids.get(i);
+ try {
+ int freezeInfo = getBinderFreezeInfo(current);
+
+ if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
+ killProcess(current, "Sync transaction while frozen",
+ ApplicationExitInfo.REASON_FREEZER,
+ ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION);
+
+ // No need to check async transactions in this case
+ continue;
+ }
+
+ if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {
+ if (pidsAsync != null) {
+ pidsAsync.add(current);
+ }
+ if (DEBUG_FREEZER) {
+ Slog.w(TAG_AM, "pid " + current
+ + " received async transactions while frozen");
+ }
+ }
+ } catch (Exception e) {
+ // The process has died. No need to kill it again.
+ Slog.w(TAG_AM, "Unable to query binder frozen stats for pid " + current);
+ }
+ }
+
+ // TODO: when kernel binder driver supports, poll the binder status directly.
+ // Binderfs stats, like other debugfs files, is not a reliable interface. But it's the
+ // only true source for now. The following code checks all frozen PIDs. If any of them
+ // is running out of async binder buffer, kill it. Otherwise it will be killed at a
+ // later time when AMS unfreezes it, which causes race issues.
+ if (pidsAsync == null || pidsAsync.size() == 0) {
+ return;
+ }
+ new BinderfsStatsReader().handleFreeAsyncSpace(
+ // Check if the frozen process has pending async calls
+ pidsAsync::contains,
+
+ // Kill the current process if it's running out of async binder space
+ (current, free) -> {
+ if (free < mFreezerBinderAsyncThreshold) {
+ Slog.w(TAG_AM, "pid " + current
+ + " has " + free + " free async space, killing");
+ killProcess(current, "Async binder space running out while frozen",
+ ApplicationExitInfo.REASON_FREEZER,
+ ApplicationExitInfo.SUBREASON_FREEZER_BINDER_ASYNC_FULL);
+ }
+ },
+
+ // Log the error if binderfs stats can't be accesses or correctly parsed
+ exception -> Slog.e(TAG_AM, "Unable to parse binderfs stats"));
+ }
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9bbfc8f..16f55e8 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -125,6 +125,7 @@
"arc_next",
"bluetooth",
"build",
+ "biometrics",
"biometrics_framework",
"biometrics_integration",
"camera_platform",
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
index 6bed42b..5b77c52 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -23,6 +23,7 @@
import android.compat.annotation.EnabledSince;
import android.hardware.broadcastradio.AmFmRegionConfig;
import android.hardware.broadcastradio.Announcement;
+import android.hardware.broadcastradio.ConfigFlag;
import android.hardware.broadcastradio.DabTableEntry;
import android.hardware.broadcastradio.IdentifierType;
import android.hardware.broadcastradio.Metadata;
@@ -32,6 +33,7 @@
import android.hardware.broadcastradio.Properties;
import android.hardware.broadcastradio.Result;
import android.hardware.broadcastradio.VendorKeyValue;
+import android.hardware.radio.Flags;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
@@ -45,6 +47,7 @@
import android.util.ArraySet;
import android.util.IntArray;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.Slogf;
import java.util.ArrayList;
@@ -65,13 +68,22 @@
/**
* With RADIO_U_VERSION_REQUIRED enabled, 44-bit DAB identifier
- * {@link IdentifierType#DAB_SID_EXT} from broadcast radio HAL can be passed as
- * {@link ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} to {@link RadioTuner}.
+ * {@code IdentifierType#DAB_SID_EXT} from broadcast radio HAL can be passed as
+ * {@code ProgramSelector#IDENTIFIER_TYPE_DAB_DMB_SID_EXT} to {@code RadioTuner}.
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static final long RADIO_U_VERSION_REQUIRED = 261770108L;
+ /**
+ * With RADIO_V_VERSION_REQUIRED enabled, identifier types, config flags and metadata added
+ * in V for HD radio can be passed to {@code RadioTuner} by
+ * {@code android.hardware.radio.ITunerCallback}
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public static final long RADIO_V_VERSION_REQUIRED = 302589903L;
+
private ConversionUtils() {
throw new UnsupportedOperationException("ConversionUtils class is noninstantiable");
}
@@ -81,6 +93,11 @@
return CompatChanges.isChangeEnabled(RADIO_U_VERSION_REQUIRED, uid);
}
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ static boolean isAtLeastV(int uid) {
+ return CompatChanges.isChangeEnabled(RADIO_V_VERSION_REQUIRED, uid);
+ }
+
static RuntimeException throwOnError(RuntimeException halException, String action) {
if (!(halException instanceof ServiceSpecificException)) {
return new ParcelableException(new RuntimeException(
@@ -181,6 +198,7 @@
// TODO(b/69958423): verify AM/FM with frequency range
return ProgramSelector.PROGRAM_TYPE_FM;
case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
+ case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME:
// TODO(b/69958423): verify AM/FM with frequency range
return ProgramSelector.PROGRAM_TYPE_FM_HD;
case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
@@ -195,6 +213,12 @@
case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
return ProgramSelector.PROGRAM_TYPE_SXM;
+ default:
+ if (Flags.hdRadioImproved()) {
+ if (idType == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION) {
+ return ProgramSelector.PROGRAM_TYPE_FM_HD;
+ }
+ }
}
if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
&& idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
@@ -322,9 +346,16 @@
static ProgramIdentifier identifierToHalProgramIdentifier(ProgramSelector.Identifier id) {
ProgramIdentifier hwId = new ProgramIdentifier();
- hwId.type = id.getType();
if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
hwId.type = IdentifierType.DAB_SID_EXT;
+ } else if (Flags.hdRadioImproved()) {
+ if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION) {
+ hwId.type = IdentifierType.HD_STATION_LOCATION;
+ } else {
+ hwId.type = id.getType();
+ }
+ } else {
+ hwId.type = id.getType();
}
long value = id.getValue();
if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT) {
@@ -344,6 +375,12 @@
int idType;
if (id.type == IdentifierType.DAB_SID_EXT) {
idType = ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ } else if (id.type == IdentifierType.HD_STATION_LOCATION) {
+ if (Flags.hdRadioImproved()) {
+ idType = ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION;
+ } else {
+ return null;
+ }
} else {
idType = id.type;
}
@@ -375,7 +412,12 @@
ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
ArrayList<ProgramIdentifier> secondaryIdList = new ArrayList<>(secondaryIds.length);
for (int i = 0; i < secondaryIds.length; i++) {
- secondaryIdList.add(identifierToHalProgramIdentifier(secondaryIds[i]));
+ ProgramIdentifier hwId = identifierToHalProgramIdentifier(secondaryIds[i]);
+ if (hwId.type != IdentifierType.INVALID) {
+ secondaryIdList.add(hwId);
+ } else {
+ Slogf.w(TAG, "Invalid secondary id: %s", secondaryIds[i]);
+ }
}
hwSel.secondaryIds = secondaryIdList.toArray(ProgramIdentifier[]::new);
if (!isValidHalProgramSelector(hwSel)) {
@@ -400,7 +442,12 @@
List<ProgramSelector.Identifier> secondaryIdList = new ArrayList<>();
for (int i = 0; i < sel.secondaryIds.length; i++) {
if (sel.secondaryIds[i] != null) {
- secondaryIdList.add(identifierFromHalProgramIdentifier(sel.secondaryIds[i]));
+ ProgramSelector.Identifier id = identifierFromHalProgramIdentifier(
+ sel.secondaryIds[i]);
+ if (id == null) {
+ Slogf.e(TAG, "invalid secondary id: %s", sel.secondaryIds[i]);
+ }
+ secondaryIdList.add(id);
}
}
@@ -411,11 +458,13 @@
/* vendorIds= */ null);
}
- private static RadioMetadata radioMetadataFromHalMetadata(Metadata[] meta) {
+ @VisibleForTesting
+ static RadioMetadata radioMetadataFromHalMetadata(Metadata[] meta) {
RadioMetadata.Builder builder = new RadioMetadata.Builder();
for (int i = 0; i < meta.length; i++) {
- switch (meta[i].getTag()) {
+ int tag = meta[i].getTag();
+ switch (tag) {
case Metadata.rdsPs:
builder.putString(RadioMetadata.METADATA_KEY_RDS_PS, meta[i].getRdsPs());
break;
@@ -472,10 +521,52 @@
meta[i].getDabComponentNameShort());
break;
default:
- Slogf.w(TAG, "Ignored unknown metadata entry: %s", meta[i]);
+ if (Flags.hdRadioImproved()) {
+ switch (tag) {
+ case Metadata.genre:
+ builder.putString(RadioMetadata.METADATA_KEY_GENRE,
+ meta[i].getGenre());
+ break;
+ case Metadata.commentShortDescription:
+ builder.putString(
+ RadioMetadata.METADATA_KEY_COMMENT_SHORT_DESCRIPTION,
+ meta[i].getCommentShortDescription());
+ break;
+ case Metadata.commentActualText:
+ builder.putString(RadioMetadata.METADATA_KEY_COMMENT_ACTUAL_TEXT,
+ meta[i].getCommentActualText());
+ break;
+ case Metadata.commercial:
+ builder.putString(RadioMetadata.METADATA_KEY_COMMERCIAL,
+ meta[i].getCommercial());
+ break;
+ case Metadata.ufids:
+ builder.putStringArray(RadioMetadata.METADATA_KEY_UFIDS,
+ meta[i].getUfids());
+ break;
+ case Metadata.hdStationNameShort:
+ builder.putString(RadioMetadata.METADATA_KEY_HD_STATION_NAME_SHORT,
+ meta[i].getHdStationNameShort());
+ break;
+ case Metadata.hdStationNameLong:
+ builder.putString(RadioMetadata.METADATA_KEY_HD_STATION_NAME_LONG,
+ meta[i].getHdStationNameLong());
+ break;
+ case Metadata.hdSubChannelsAvailable:
+ builder.putInt(RadioMetadata.METADATA_KEY_HD_SUBCHANNELS_AVAILABLE,
+ meta[i].getHdSubChannelsAvailable());
+ break;
+ default:
+ Slogf.w(TAG, "Ignored unknown metadata entry: %s with HD radio flag"
+ + " enabled", meta[i]);
+ break;
+ }
+ } else {
+ Slogf.w(TAG, "Ignored unknown metadata entry: %s with HD radio flag "
+ + "disabled", meta[i]);
+ }
break;
}
-
}
return builder.build();
@@ -547,7 +638,13 @@
}
Iterator<ProgramSelector.Identifier> idIterator = filter.getIdentifiers().iterator();
while (idIterator.hasNext()) {
- identifiersList.add(identifierToHalProgramIdentifier(idIterator.next()));
+ ProgramSelector.Identifier id = idIterator.next();
+ ProgramIdentifier hwId = identifierToHalProgramIdentifier(id);
+ if (hwId.type != IdentifierType.INVALID) {
+ identifiersList.add(hwId);
+ } else {
+ Slogf.w(TAG, "Invalid identifiers: %s", id);
+ }
}
hwFilter.identifierTypes = identifierTypeList.toArray();
@@ -558,20 +655,26 @@
return hwFilter;
}
- private static boolean isNewIdentifierInU(ProgramSelector.Identifier id) {
- return id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ private static boolean identifierMeetsSdkVersionRequirement(ProgramSelector.Identifier id,
+ int uid) {
+ if (Flags.hdRadioImproved() && !isAtLeastV(uid)) {
+ if (id.getType() == ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION) {
+ return false;
+ }
+ }
+ if (!isAtLeastU(uid)) {
+ return id.getType() != ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ }
+ return true;
}
static boolean programSelectorMeetsSdkVersionRequirement(ProgramSelector sel, int uid) {
- if (isAtLeastU(uid)) {
- return true;
- }
- if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
+ if (!identifierMeetsSdkVersionRequirement(sel.getPrimaryId(), uid)) {
return false;
}
ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
for (int i = 0; i < secondaryIds.length; i++) {
- if (isNewIdentifierInU(secondaryIds[i])) {
+ if (!identifierMeetsSdkVersionRequirement(secondaryIds[i], uid)) {
return false;
}
}
@@ -579,14 +682,11 @@
}
static boolean programInfoMeetsSdkVersionRequirement(RadioManager.ProgramInfo info, int uid) {
- if (isAtLeastU(uid)) {
- return true;
- }
if (!programSelectorMeetsSdkVersionRequirement(info.getSelector(), uid)) {
return false;
}
- if (isNewIdentifierInU(info.getLogicallyTunedTo())
- || isNewIdentifierInU(info.getPhysicallyTunedTo())) {
+ if (!identifierMeetsSdkVersionRequirement(info.getLogicallyTunedTo(), uid)
+ || !identifierMeetsSdkVersionRequirement(info.getPhysicallyTunedTo(), uid)) {
return false;
}
if (info.getRelatedContent() == null) {
@@ -594,7 +694,7 @@
}
Iterator<ProgramSelector.Identifier> relatedContentIt = info.getRelatedContent().iterator();
while (relatedContentIt.hasNext()) {
- if (isNewIdentifierInU(relatedContentIt.next())) {
+ if (!identifierMeetsSdkVersionRequirement(relatedContentIt.next(), uid)) {
return false;
}
}
@@ -602,9 +702,6 @@
}
static ProgramList.Chunk convertChunkToTargetSdkVersion(ProgramList.Chunk chunk, int uid) {
- if (isAtLeastU(uid)) {
- return chunk;
- }
Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator();
while (modifiedIterator.hasNext()) {
@@ -617,13 +714,21 @@
Iterator<UniqueProgramIdentifier> removedIterator = chunk.getRemoved().iterator();
while (removedIterator.hasNext()) {
UniqueProgramIdentifier id = removedIterator.next();
- if (!isNewIdentifierInU(id.getPrimaryId())) {
+ if (identifierMeetsSdkVersionRequirement(id.getPrimaryId(), uid)) {
removed.add(id);
}
}
return new ProgramList.Chunk(chunk.isPurge(), chunk.isComplete(), modified, removed);
}
+ static boolean configFlagMeetsSdkVersionRequirement(int configFlag, int uid) {
+ if (!Flags.hdRadioImproved() || !isAtLeastV(uid)) {
+ return configFlag != ConfigFlag.FORCE_ANALOG_AM
+ && configFlag != ConfigFlag.FORCE_ANALOG_FM;
+ }
+ return true;
+ }
+
public static android.hardware.radio.Announcement announcementFromHalAnnouncement(
Announcement hwAnnouncement) {
return new android.hardware.radio.Announcement(
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index 2ae7f95..4b3444d 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -167,6 +167,11 @@
fireLater(() -> {
synchronized (mLock) {
fanoutAidlCallbackLocked((cb, uid) -> {
+ if (!ConversionUtils.configFlagMeetsSdkVersionRequirement(flag, uid)) {
+ Slogf.e(TAG, "onConfigFlagUpdated: cannot send program info "
+ + "requiring higher target SDK version");
+ return;
+ }
cb.onConfigFlagUpdated(flag, value);
});
}
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index f9bc8dc..5bb5c53 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -243,6 +243,7 @@
public String mUserTag;
public int mVideoStabilizationMode;
public boolean mUsedUltraWide;
+ public boolean mUsedZoomOverride;
public final long mLogId;
public final int mSessionIndex;
@@ -271,7 +272,7 @@
long resultErrorCount, boolean deviceError,
List<CameraStreamStats> streamStats, String userTag,
int videoStabilizationMode, boolean usedUltraWide,
- CameraExtensionSessionStats extStats) {
+ boolean usedZoomOverride, CameraExtensionSessionStats extStats) {
if (mCompleted) {
return;
}
@@ -285,6 +286,7 @@
mUserTag = userTag;
mVideoStabilizationMode = videoStabilizationMode;
mUsedUltraWide = usedUltraWide;
+ mUsedZoomOverride = usedZoomOverride;
mExtSessionStats = extStats;
if (CameraServiceProxy.DEBUG) {
Slog.v(TAG, "A camera facing " + cameraFacingToString(mCameraFacing) +
@@ -877,6 +879,9 @@
String ultrawideDebug = Flags.logUltrawideUsage()
? ", wideAngleUsage " + e.mUsedUltraWide
: "";
+ String zoomOverrideDebug = Flags.logZoomOverrideUsage()
+ ? ", zoomOverrideUsage " + e.mUsedZoomOverride
+ : "";
Slog.v(TAG, "CAMERA_ACTION_EVENT: action " + e.mAction
+ " clientName " + e.mClientName
@@ -895,6 +900,7 @@
+ ", userTag is " + e.mUserTag
+ ", videoStabilizationMode " + e.mVideoStabilizationMode
+ ultrawideDebug
+ + zoomOverrideDebug
+ ", logId " + e.mLogId
+ ", sessionIndex " + e.mSessionIndex
+ ", mExtSessionStats {type " + extensionType
@@ -960,7 +966,8 @@
MessageNano.toByteArray(streamProtos[4]),
e.mUserTag, e.mVideoStabilizationMode,
e.mLogId, e.mSessionIndex,
- extensionType, extensionIsAdvanced, e.mUsedUltraWide);
+ extensionType, extensionIsAdvanced, e.mUsedUltraWide,
+ e.mUsedZoomOverride);
}
}
@@ -1158,6 +1165,8 @@
String userTag = cameraState.getUserTag();
int videoStabilizationMode = cameraState.getVideoStabilizationMode();
boolean usedUltraWide = Flags.logUltrawideUsage() ? cameraState.getUsedUltraWide() : false;
+ boolean usedZoomOverride =
+ Flags.logZoomOverrideUsage() ? cameraState.getUsedZoomOverride() : false;
long logId = cameraState.getLogId();
int sessionIdx = cameraState.getSessionIndex();
CameraExtensionSessionStats extSessionStats = cameraState.getExtensionSessionStats();
@@ -1216,7 +1225,7 @@
oldEvent.markCompleted(/*internalReconfigure*/0, /*requestCount*/0,
/*resultErrorCount*/0, /*deviceError*/false, streamStats,
/*userTag*/"", /*videoStabilizationMode*/-1, /*usedUltraWide*/false,
- new CameraExtensionSessionStats());
+ /*usedZoomOverride*/false, new CameraExtensionSessionStats());
mCameraUsageHistory.add(oldEvent);
}
break;
@@ -1227,7 +1236,8 @@
doneEvent.markCompleted(internalReconfigureCount, requestCount,
resultErrorCount, deviceError, streamStats, userTag,
- videoStabilizationMode, usedUltraWide, extSessionStats);
+ videoStabilizationMode, usedUltraWide, usedZoomOverride,
+ extSessionStats);
mCameraUsageHistory.add(doneEvent);
// Do not double count device error
deviceError = false;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 087cf20..e475fe6 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -31,6 +31,7 @@
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
@@ -57,6 +58,7 @@
import android.app.compat.CompatChanges;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.flags.Flags;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.BroadcastReceiver;
@@ -1482,7 +1484,12 @@
if ((flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
}
- if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0 && virtualDevice != null) {
+ // Put the display in the virtual device's display group only if it's not a mirror display,
+ // and if it doesn't need its own display group. So effectively, mirror displays go into the
+ // default display group.
+ if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0
+ && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
+ && virtualDevice != null) {
flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
}
@@ -1516,11 +1523,16 @@
if (callingUid != Process.SYSTEM_UID
&& (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0) {
- if (!canProjectVideo(projection)) {
+ // Only a valid media projection or a virtual device can create a mirror virtual
+ // display.
+ if (!canProjectVideo(projection)
+ && !isMirroringSupportedByVirtualDevice(virtualDevice)) {
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 "
- + "display.");
+ + "display. In order to create a virtual display that does not perform"
+ + "screen sharing (mirroring), please use the flag"
+ + "VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY.");
}
}
if (callingUid != Process.SYSTEM_UID && (flags & VIRTUAL_DISPLAY_FLAG_SECURE) != 0) {
@@ -1540,6 +1552,15 @@
}
}
+ // Mirror virtual displays created by a virtual device are not allowed to show
+ // presentations.
+ if (virtualDevice != null && (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0
+ && (flags & VIRTUAL_DISPLAY_FLAG_PRESENTATION) != 0) {
+ Slog.d(TAG, "Mirror displays created by a virtual device cannot show "
+ + "presentations, hence ignoring flag VIRTUAL_DISPLAY_FLAG_PRESENTATION.");
+ flags &= ~VIRTUAL_DISPLAY_FLAG_PRESENTATION;
+ }
+
if (callingUid != Process.SYSTEM_UID
&& (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
// The virtualDevice instance has been validated above using isValidVirtualDevice
@@ -1739,6 +1760,10 @@
return -1;
}
+ private static boolean isMirroringSupportedByVirtualDevice(IVirtualDevice virtualDevice) {
+ return Flags.interactiveScreenMirror() && virtualDevice != null;
+ }
+
private void resizeVirtualDisplayInternal(IBinder appToken,
int width, int height, int densityDpi) {
synchronized (mSyncRoot) {
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index dcac8c9..66a76cc 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -8,6 +8,13 @@
}
flag {
+ name: "modes_api"
+ namespace: "systemui"
+ description: "This flag controls new and updated DND apis"
+ bug: "300477976"
+}
+
+flag {
name: "polite_notifications"
namespace: "systemui"
description: "This flag controls the polite notification feature"
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 8e767e7..8bf903a 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.CONTROL_KEYGUARD;
import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
+import static android.content.pm.Flags.sdkLibIndependence;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
@@ -187,7 +188,9 @@
List<VersionedPackage> libClientPackages =
computer.getPackagesUsingSharedLibrary(libraryInfo,
MATCH_KNOWN_PACKAGES, Process.SYSTEM_UID, currUserId);
- if (!ArrayUtils.isEmpty(libClientPackages)) {
+ boolean allowSdkLibIndependence =
+ (pkg.getSdkLibraryName() != null) && sdkLibIndependence();
+ if (!ArrayUtils.isEmpty(libClientPackages) && !allowSdkLibIndependence) {
Slog.w(TAG, "Not removing package " + pkg.getManifestPackageName()
+ " hosting lib " + libraryInfo.getName() + " version "
+ libraryInfo.getLongVersion() + " used by " + libClientPackages
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index d958222..9213d96 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -18,6 +18,8 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.net.vcn.FeatureFlags;
+import android.net.vcn.FeatureFlagsImpl;
import android.os.Looper;
import java.util.Objects;
@@ -31,6 +33,7 @@
@NonNull private final Context mContext;
@NonNull private final Looper mLooper;
@NonNull private final VcnNetworkProvider mVcnNetworkProvider;
+ @NonNull private final FeatureFlags mFeatureFlags;
private final boolean mIsInTestMode;
public VcnContext(
@@ -42,6 +45,9 @@
mLooper = Objects.requireNonNull(looper, "Missing looper");
mVcnNetworkProvider = Objects.requireNonNull(vcnNetworkProvider, "Missing networkProvider");
mIsInTestMode = isInTestMode;
+
+ // Auto-generated class
+ mFeatureFlags = new FeatureFlagsImpl();
}
@NonNull
@@ -63,6 +69,11 @@
return mIsInTestMode;
}
+ @NonNull
+ public FeatureFlags getFeatureFlags() {
+ return mFeatureFlags;
+ }
+
/**
* Verifies that the caller is running on the VcnContext Thread.
*
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index d480ddb..54c97dd 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -1222,6 +1222,14 @@
@VisibleForTesting(visibility = Visibility.PRIVATE)
void setSafeModeAlarm() {
+ final boolean isFlagSafeModeConfigEnabled = mVcnContext.getFeatureFlags().safeModeConfig();
+ logVdbg("isFlagSafeModeConfigEnabled " + isFlagSafeModeConfigEnabled);
+
+ if (isFlagSafeModeConfigEnabled && !mConnectionConfig.isSafeModeEnabled()) {
+ logVdbg("setSafeModeAlarm: safe mode disabled");
+ return;
+ }
+
logVdbg("Setting safe mode alarm; mCurrentToken: " + mCurrentToken);
// Only schedule a NEW alarm if none is already set.
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 9b7b8de..cb4cf9d 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -356,15 +356,23 @@
public String toString() {
StringBuilder builder = new StringBuilder();
- if (mBackground) {
- builder.append("Background ");
- }
- builder.append("Activity start allowed: " + mMessage + ".");
- builder.append("BAL Code: ");
+ builder.append(". BAL Code: ");
builder.append(balCodeToString(mCode));
- if (mProcessInfo != null) {
+ if (DEBUG_ACTIVITY_STARTS) {
builder.append(" ");
- builder.append(mProcessInfo);
+ if (mBackground) {
+ builder.append("Background ");
+ }
+ builder.append("Activity start ");
+ if (mCode == BAL_BLOCK) {
+ builder.append("denied");
+ } else {
+ builder.append("allowed: ").append(mMessage);
+ }
+ if (mProcessInfo != null) {
+ builder.append(" ");
+ builder.append(mProcessInfo);
+ }
}
return builder.toString();
}
diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
index e849589..9a32dc8 100644
--- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
+++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java
@@ -105,24 +105,23 @@
// Allow if the proc is instrumenting with background activity starts privs.
if (hasBackgroundActivityStartPrivileges) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
- "Activity start allowed: process instrumenting with background "
- + "activity starts privileges");
+ "process instrumenting with background activity starts privileges");
}
// Allow if the flag was explicitly set.
if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
- "Activity start allowed: process allowed by token");
+ "process allowed by token");
}
// Allow if the caller is bound by a UID that's currently foreground.
if (isBoundByForegroundUid()) {
return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false,
- "Activity start allowed: process bound by foreground uid");
+ "process bound by foreground uid");
}
// Allow if the caller has an activity in any foreground task.
if (hasActivityInVisibleTask
&& (appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY)) {
return new BalVerdict(BAL_ALLOW_FOREGROUND, /*background*/ false,
- "Activity start allowed: process has activity in foreground task");
+ "process has activity in foreground task");
}
// If app switching is not allowed, we ignore all the start activity grace period
@@ -138,8 +137,7 @@
if (lastActivityLaunchTime > lastStopAppSwitchesTime
|| lastActivityFinishTime > lastStopAppSwitchesTime) {
return new BalVerdict(BAL_ALLOW_GRACE_PERIOD, /*background*/ true,
- "Activity start allowed: within "
- + ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period");
+ "within " + ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period");
}
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "[Process(" + pid + ")] Activity start within "
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index 15a0445..1dc9493 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -29,6 +29,7 @@
import android.util.Slog;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
+import com.android.wm.shell.Flags;
/**
* The class that defines default launch params for tasks in desktop mode
@@ -40,6 +41,7 @@
private static final boolean DEBUG = false;
// Desktop mode feature flags.
+ private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing();
private static final boolean DESKTOP_MODE_PROTO2_SUPPORTED =
SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false);
// Override default freeform task width when desktop mode is enabled. In dips.
@@ -91,7 +93,7 @@
// previous windowing mode to be restored even if the desktop mode state has changed.
// Let task launches inherit the windowing mode from the source task if available, which
// should have the desired windowing mode set by WM Shell. See b/286929122.
- if (DESKTOP_MODE_PROTO2_SUPPORTED && source != null && source.getTask() != null) {
+ if (isDesktopModeSupported() && source != null && source.getTask() != null) {
final Task sourceTask = source.getTask();
outParams.mWindowingMode = sourceTask.getWindowingMode();
appendLog("inherit-from-source=" + outParams.mWindowingMode);
@@ -140,6 +142,12 @@
/** Whether desktop mode is supported. */
static boolean isDesktopModeSupported() {
+ // Check for aconfig flag first
+ if (ENABLE_DESKTOP_WINDOWING) {
+ return true;
+ }
+ // Fall back to sysprop flag
+ // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding
return DESKTOP_MODE_PROTO2_SUPPORTED;
}
}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index c81105a..4a467df 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2054,6 +2054,12 @@
Transition.ReadyCondition pipChangesApplied = new Transition.ReadyCondition("movedToPip");
transitionController.waitFor(pipChangesApplied);
mService.deferWindowLayout();
+ boolean localVisibilityDeferred = false;
+ // If the caller is from WindowOrganizerController, it should be already deferred.
+ if (!mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
+ mTaskSupervisor.setDeferRootVisibilityUpdate(true);
+ localVisibilityDeferred = true;
+ }
try {
// This will change the root pinned task's windowing mode to its original mode, ensuring
// we only have one root task that is in pinned mode.
@@ -2225,14 +2231,11 @@
mService.mTaskFragmentOrganizerController.dispatchPendingInfoChangedEvent(
organizedTf);
}
-
- if (taskDisplayArea.getFocusedRootTask() == rootTask) {
- taskDisplayArea.clearPreferredTopFocusableRootTask();
- }
} finally {
mService.continueWindowLayout();
try {
- if (!isPip2ExperimentEnabled()) {
+ if (localVisibilityDeferred) {
+ mTaskSupervisor.setDeferRootVisibilityUpdate(false);
ensureActivitiesVisible(null, 0, false /* preserveWindows */);
}
} finally {
diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java
index 2be2a1a..01fa39b 100644
--- a/services/core/java/com/android/server/wm/SnapshotController.java
+++ b/services/core/java/com/android/server/wm/SnapshotController.java
@@ -98,9 +98,14 @@
final TaskFragment tf = info.mContainer.asTaskFragment();
final ActivityRecord ar = tf != null ? tf.getTopMostActivity()
: info.mContainer.asActivityRecord();
- final boolean taskVis = ar != null && ar.getTask().isVisibleRequested();
- if (ar != null && !ar.isVisibleRequested() && taskVis) {
- mActivitySnapshotController.recordSnapshot(ar);
+ if (ar != null && !ar.isVisibleRequested() && ar.getTask().isVisibleRequested()) {
+ final WindowState mainWindow = ar.findMainWindow(false);
+ // Only capture activity snapshot if this app has adapted to back predict
+ if (mainWindow != null
+ && mainWindow.getOnBackInvokedCallbackInfo() != null
+ && mainWindow.getOnBackInvokedCallbackInfo().isSystemCallback()) {
+ mActivitySnapshotController.recordSnapshot(ar);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index ae794a8..f0a6654 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -412,7 +412,8 @@
// wasContained} restricts the preferred root task is set only when moving an existing
// root task to top instead of adding a new root task that may be too early (e.g. in the
// middle of launching or reparenting).
- final boolean isTopFocusableTask = moveToTop && child.isTopActivityFocusable();
+ final boolean isTopFocusableTask = moveToTop && child != mRootPinnedTask
+ && child.isTopActivityFocusable();
if (isTopFocusableTask) {
mPreferredTopFocusableRootTask =
child.shouldBeVisible(null /* starting */) ? child : null;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 9594c65..b23ffa8 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1051,7 +1051,8 @@
* @return true if we are *guaranteed* to enter-pip. This means we return false if there's
* a chance we won't thus legacy-entry (via pause+userLeaving) will return false.
*/
- private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) {
+ private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar,
+ @Nullable ActivityRecord resuming) {
if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) {
return false;
}
@@ -1096,8 +1097,7 @@
try {
// If not going auto-pip, the activity should be paused with user-leaving.
mController.mAtm.mTaskSupervisor.mUserLeaving = true;
- ar.getTaskFragment().startPausing(false /* uiSleeping */,
- null /* resuming */, "finishTransition");
+ ar.getTaskFragment().startPausing(false /* uiSleeping */, resuming, "finishTransition");
} finally {
mController.mAtm.mTaskSupervisor.mUserLeaving = false;
}
@@ -1195,7 +1195,9 @@
final boolean isScreenOff = ar.mDisplayContent == null
|| ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
- final boolean commitVisibility = !checkEnterPipOnFinish(ar);
+ final ActivityRecord resuming = getVisibleTransientLaunch(
+ ar.getTaskDisplayArea());
+ final boolean commitVisibility = !checkEnterPipOnFinish(ar, resuming);
// Avoid commit visibility if entering pip or else we will get a sudden
// "flash" / surface going invisible for a split second.
if (commitVisibility) {
@@ -1414,6 +1416,22 @@
mController.mSnapshotController.onTransitionFinish(mType, mTargets);
}
+ @Nullable
+ private ActivityRecord getVisibleTransientLaunch(TaskDisplayArea taskDisplayArea) {
+ if (mTransientLaunches == null) return null;
+ for (int i = mTransientLaunches.size() - 1; i >= 0; --i) {
+ final ActivityRecord candidateActivity = mTransientLaunches.keyAt(i);
+ if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) {
+ continue;
+ }
+ if (!candidateActivity.isVisible()) {
+ continue;
+ }
+ return candidateActivity;
+ }
+ return null;
+ }
+
void abort() {
// This calls back into itself via controller.abort, so just early return here.
if (mState == STATE_ABORT) return;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 0d024d6..56e385d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -65,6 +65,7 @@
import android.os.FactoryTest;
import android.os.FileUtils;
import android.os.IBinder;
+import android.os.IBinderCallback;
import android.os.IIncidentManager;
import android.os.Looper;
import android.os.Message;
@@ -985,6 +986,14 @@
}
}
+ // Set binder transaction callback after starting system services
+ Binder.setTransactionCallback(new IBinderCallback() {
+ @Override
+ public void onTransactionError(int pid, int code, int flags, int err) {
+ mActivityManagerService.frozenBinderTransactionDetected(pid, code, flags, err);
+ }
+ });
+
// Loop forever.
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
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 16d72e4..163d248 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -18,10 +18,13 @@
import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
+import static android.Manifest.permission.CAPTURE_VIDEO_OUTPUT;
import static android.Manifest.permission.MANAGE_DISPLAYS;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
@@ -60,6 +63,7 @@
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.IVirtualDeviceManager;
import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.flags.Flags;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.Context;
import android.content.ContextWrapper;
@@ -92,6 +96,7 @@
import android.os.MessageQueue;
import android.os.Process;
import android.os.RemoteException;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.ContentRecordingSession;
import android.view.Display;
import android.view.DisplayCutout;
@@ -181,6 +186,8 @@
public TestRule compatChangeRule = new PlatformCompatChangeRule();
@Rule(order = 1)
public Expect expect = Expect.create();
+ @Rule
+ public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private Context mContext;
@@ -312,7 +319,6 @@
@Mock DisplayDeviceConfig mMockDisplayDeviceConfig;
@Mock PackageManagerInternal mMockPackageManagerInternal;
-
@Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor;
@Mock DisplayManagerFlags mMockFlags;
@@ -320,6 +326,7 @@
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false);
+ mSetFlagsRule.disableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
LocalServices.removeServiceForTest(InputManagerInternal.class);
LocalServices.addService(InputManagerInternal.class, mMockInputManagerInternal);
@@ -1140,6 +1147,236 @@
0);
}
+ /**
+ * Tests that it's not allowed to create an auto-mirror virtual display without
+ * CAPTURE_VIDEO_OUTPUT permission or a virtual device.
+ */
+ @Test
+ public void createAutoMirrorDisplay_withoutPermission_withoutVirtualDevice_throwsException() {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ registerDefaultDisplays(displayManager);
+ when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+ 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 */,
+ null /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+ });
+ }
+
+ /**
+ * Tests that it's not allowed to create an auto-mirror virtual display when display mirroring
+ * is not supported in a virtual device.
+ */
+ @Test
+ public void createAutoMirrorDisplay_virtualDeviceDoesntSupportMirroring_throwsException()
+ throws Exception {
+ mSetFlagsRule.disableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+ registerDefaultDisplays(displayManager);
+ when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+ when(mContext.checkCallingPermission(CAPTURE_VIDEO_OUTPUT)).thenReturn(
+ PackageManager.PERMISSION_DENIED);
+ IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+ when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ 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.
+ */
+ @Test
+ public void createAutoMirrorVirtualDisplay_addsDisplayToDefaultDisplayGroup() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ 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(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ // Create an auto-mirror virtual display using a virtual device.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR)
+ .setUniqueId("uniqueId --- default display group");
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+
+ // The virtual display should be in the default display group.
+ assertEquals(Display.DEFAULT_DISPLAY_GROUP,
+ localService.getDisplayInfo(displayId).displayGroupId);
+ }
+
+ /**
+ * Tests that the virtual display mirrors the default display when created with
+ * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device.
+ */
+ @Test
+ public void createAutoMirrorVirtualDisplay_mirrorsDefaultDisplay() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ 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(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ // Create an auto-mirror virtual display using a virtual device.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR)
+ .setUniqueId("uniqueId --- mirror display");
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+
+ // The virtual display should mirror the default display.
+ assertEquals(Display.DEFAULT_DISPLAY, localService.getDisplayIdToMirror(displayId));
+ }
+
+ /**
+ * Tests that the virtual display does not mirror any other display when created with
+ * VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY using a virtual device.
+ */
+ @Test
+ public void createOwnContentOnlyVirtualDisplay_doesNotMirrorAnyDisplay() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ 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(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ // Create an auto-mirror virtual display using a virtual device.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY)
+ .setUniqueId("uniqueId --- own content only display");
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+
+ // The virtual display should not mirror any display.
+ assertEquals(Display.INVALID_DISPLAY, localService.getDisplayIdToMirror(displayId));
+ // The virtual display should have FLAG_OWN_CONTENT_ONLY set.
+ assertEquals(DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY,
+ (displayManager.getDisplayDeviceInfoInternal(displayId).flags
+ & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY));
+ }
+
+ /**
+ * Tests that the virtual display should not be always unlocked when created with
+ * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device.
+ */
+ @Test
+ public void createAutoMirrorVirtualDisplay_flagAlwaysUnlockedNotSet() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ 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(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+ when(mContext.checkCallingPermission(ADD_ALWAYS_UNLOCKED_DISPLAY))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
+ // Create an auto-mirror virtual display using a virtual device.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
+ | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+ .setUniqueId("uniqueId --- mirror display");
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+
+ // The virtual display should not have FLAG_ALWAYS_UNLOCKED set.
+ assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags
+ & DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED));
+ }
+
+ /**
+ * Tests that the virtual display should not allow presentation when created with
+ * VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR using a virtual device.
+ */
+ @Test
+ public void createAutoMirrorVirtualDisplay_flagPresentationNotSet() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ 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(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ // Create an auto-mirror virtual display using a virtual device.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
+ | VIRTUAL_DISPLAY_FLAG_PRESENTATION)
+ .setUniqueId("uniqueId --- mirror display");
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+
+ // The virtual display should not have FLAG_PRESENTATION set.
+ assertEquals(0, (displayManager.getDisplayDeviceInfoInternal(displayId).flags
+ & DisplayDeviceInfo.FLAG_PRESENTATION));
+ }
+
@Test
public void testGetDisplayIdToMirror() throws Exception {
DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
index a7c8a6c..b732d38 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/GenericWindowPolicyControllerTest.java
@@ -150,7 +150,7 @@
@Test
public void openNonBlockedAppOnVirtualDisplay_isNotBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -163,7 +163,7 @@
@Test
public void activityDoesNotSupportDisplayOnRemoteDevices_isBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -176,7 +176,7 @@
@Test
public void openBlockedComponentOnVirtualDisplay_isBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithBlockedComponent(BLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_PACKAGE_NAME,
@@ -189,7 +189,7 @@
@Test
public void addActivityPolicyExemption_openBlockedOnVirtualDisplay_isBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.setActivityLaunchDefaultAllowed(true);
gwpc.addActivityPolicyExemption(BLOCKED_COMPONENT);
@@ -204,7 +204,7 @@
@Test
public void openNotAllowedComponentOnBlocklistVirtualDisplay_isBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_PACKAGE_NAME,
@@ -217,7 +217,7 @@
@Test
public void addActivityPolicyExemption_openNotAllowedOnVirtualDisplay_isBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.setActivityLaunchDefaultAllowed(false);
gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT);
@@ -232,7 +232,7 @@
@Test
public void openAllowedComponentOnBlocklistVirtualDisplay_startsActivity() {
GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -245,7 +245,7 @@
@Test
public void addActivityPolicyExemption_openAllowedOnVirtualDisplay_startsActivity() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.setActivityLaunchDefaultAllowed(false);
gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT);
@@ -258,9 +258,22 @@
}
@Test
+ public void openNonBlockedAppOnMirrorVirtualDisplay_isBlocked() {
+ GenericWindowPolicyController gwpc = createGwpc();
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ true);
+
+ ActivityInfo activityInfo = getActivityInfo(
+ NONBLOCKED_APP_PACKAGE_NAME,
+ NONBLOCKED_APP_PACKAGE_NAME,
+ /* displayOnRemoteDevices */ true,
+ /* targetDisplayCategory */ null);
+ assertNoActivityLaunched(gwpc, DISPLAY_ID, activityInfo);
+ }
+
+ @Test
public void canActivityBeLaunched_mismatchingUserHandle_isBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -274,7 +287,7 @@
@Test
public void canActivityBeLaunched_blockedAppStreamingComponent_isNeverBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_APP_STREAMING_COMPONENT.getPackageName(),
@@ -288,7 +301,7 @@
public void canActivityBeLaunched_blockedAppStreamingComponentExplicitlyBlocked_isNeverBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithBlockedComponent(
BLOCKED_APP_STREAMING_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_APP_STREAMING_COMPONENT.getPackageName(),
@@ -302,7 +315,7 @@
@Test
public void canActivityBeLaunched_blockedAppStreamingComponentExemptFromStreaming_isNeverBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.setActivityLaunchDefaultAllowed(true);
gwpc.addActivityPolicyExemption(BLOCKED_APP_STREAMING_COMPONENT);
@@ -318,7 +331,7 @@
@Test
public void canActivityBeLaunched_blockedAppStreamingComponentNotAllowlisted_isNeverBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithAllowedComponent(NONBLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_APP_STREAMING_COMPONENT.getPackageName(),
@@ -332,7 +345,7 @@
@Test
public void canActivityBeLaunched_blockedAppStreamingComponentNotExemptFromBlocklist_isNeverBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.setActivityLaunchDefaultAllowed(false);
gwpc.addActivityPolicyExemption(NONBLOCKED_COMPONENT);
@@ -348,7 +361,7 @@
@Test
public void canActivityBeLaunched_customDisplayCategoryMatches_isNotBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithDisplayCategory(DISPLAY_CATEGORY);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -362,7 +375,7 @@
@Test
public void canActivityBeLaunched_customDisplayCategoryDoesNotMatch_isBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithDisplayCategory(DISPLAY_CATEGORY);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -375,7 +388,7 @@
@Test
public void canActivityBeLaunched_crossTaskLaunch_fromDefaultDisplay_isNotBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -390,7 +403,7 @@
public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_notExplicitlyBlocked_isNotBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationBlockedFor(
BLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -406,7 +419,7 @@
public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_explicitlyBlocked_isBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationBlockedFor(
BLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_PACKAGE_NAME,
@@ -421,7 +434,7 @@
public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_notAllowed_isBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationAllowed(
NONBLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_PACKAGE_NAME,
@@ -436,7 +449,7 @@
public void canActivityBeLaunched_crossTaskLaunchFromVirtualDisplay_allowed_isNotBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithCrossTaskNavigationAllowed(
NONBLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -450,7 +463,7 @@
@Test
public void canActivityBeLaunched_unsupportedWindowingMode_isBlocked() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -464,7 +477,7 @@
@Test
public void canActivityBeLaunched_permissionComponent_isBlocked() {
GenericWindowPolicyController gwpc = createGwpcWithPermissionComponent(BLOCKED_COMPONENT);
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
BLOCKED_PACKAGE_NAME,
@@ -490,7 +503,7 @@
public void onRunningAppsChanged_empty_onDisplayEmpty() {
ArraySet<Integer> uids = new ArraySet<>();
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.onRunningAppsChanged(uids);
@@ -585,7 +598,7 @@
public void onTopActivitychanged_activityListenerCallbackObserved() {
int userId = 1000;
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
gwpc.onTopActivityChanged(BLOCKED_COMPONENT, 0, userId);
verify(mActivityListener)
@@ -595,7 +608,7 @@
@Test
public void keepActivityOnWindowFlagsChanged_noChange() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -613,7 +626,7 @@
@Test
public void keepActivityOnWindowFlagsChanged_flagSecure_isAllowedAfterTM() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -631,7 +644,7 @@
@Test
public void keepActivityOnWindowFlagsChanged_systemFlagHideNonSystemOverlayWindows_isAllowedAfterTM() {
GenericWindowPolicyController gwpc = createGwpc();
- gwpc.setDisplayId(DISPLAY_ID);
+ gwpc.setDisplayId(DISPLAY_ID, /* isMirrorDisplay= */ false);
ActivityInfo activityInfo = getActivityInfo(
NONBLOCKED_APP_PACKAGE_NAME,
@@ -887,4 +900,14 @@
verify(mActivityBlockedCallback).onActivityBlocked(fromDisplay, activityInfo);
verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class));
}
+
+ private void assertNoActivityLaunched(GenericWindowPolicyController gwpc, int fromDisplay,
+ ActivityInfo activityInfo) {
+ assertThat(gwpc.canActivityBeLaunched(activityInfo, null,
+ WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID, true))
+ .isFalse();
+
+ verify(mActivityBlockedCallback, never()).onActivityBlocked(fromDisplay, activityInfo);
+ verify(mIntentListenerCallback, never()).shouldInterceptIntent(any(Intent.class));
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index a3d415e..461d637 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -59,7 +59,6 @@
import android.companion.virtual.audio.IAudioConfigChangedCallback;
import android.companion.virtual.audio.IAudioRoutingCallback;
import android.companion.virtual.flags.Flags;
-import android.companion.virtual.sensor.IVirtualSensorCallback;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorCallback;
import android.companion.virtual.sensor.VirtualSensorConfig;
@@ -250,8 +249,6 @@
@Mock
private SensorManagerInternal mSensorManagerInternalMock;
@Mock
- private IVirtualSensorCallback mVirtualSensorCallback;
- @Mock
private VirtualSensorCallback mSensorCallback;
@Mock
private IVirtualDeviceActivityListener mActivityListener;
@@ -269,7 +266,6 @@
IPowerManager mIPowerManagerMock;
@Mock
IThermalService mIThermalServiceMock;
- private PowerManager mPowerManager;
@Mock
private IAudioRoutingCallback mRoutingCallback;
@Mock
@@ -361,9 +357,10 @@
when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(
mDevicePolicyManagerMock);
- mPowerManager = new PowerManager(mContext, mIPowerManagerMock, mIThermalServiceMock,
+ PowerManager powerManager = new PowerManager(mContext, mIPowerManagerMock,
+ mIThermalServiceMock,
new Handler(TestableLooper.get(this).getLooper()));
- when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager);
+ when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);
mInputManagerMockHelper = new InputManagerMockHelper(
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
@@ -1604,6 +1601,54 @@
}
@Test
+ public void openNonBlockedAppOnMirrorDisplay_flagEnabled_cannotBeLaunched() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_INTERACTIVE_SCREEN_MIRROR);
+ when(mDisplayManagerInternalMock.getDisplayIdToMirror(anyInt()))
+ .thenReturn(Display.DEFAULT_DISPLAY);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
+ DISPLAY_ID_1);
+ doNothing().when(mContext).startActivityAsUser(any(), any(), any());
+
+ ActivityInfo activityInfo = getActivityInfo(
+ NONBLOCKED_APP_PACKAGE_NAME,
+ NONBLOCKED_APP_PACKAGE_NAME,
+ /* displayOnRemoteDevices */ true,
+ /* targetDisplayCategory */ null);
+ assertThat(gwpc.canActivityBeLaunched(activityInfo, null,
+ WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/ false))
+ .isFalse();
+ // Verify that BlockedAppStreamingActivity also doesn't launch for mirror displays.
+ Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent(
+ activityInfo, mAssociationInfo.getDisplayName());
+ verify(mContext, never()).startActivityAsUser(argThat(intent ->
+ intent.filterEquals(blockedAppIntent)), any(), any());
+ }
+
+ @Test
+ public void openNonBlockedAppOnMirrorDisplay_flagDisabled_launchesActivity() {
+ when(mDisplayManagerInternalMock.getDisplayIdToMirror(anyInt()))
+ .thenReturn(Display.DEFAULT_DISPLAY);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
+ DISPLAY_ID_1);
+ doNothing().when(mContext).startActivityAsUser(any(), any(), any());
+
+ ActivityInfo activityInfo = getActivityInfo(
+ NONBLOCKED_APP_PACKAGE_NAME,
+ NONBLOCKED_APP_PACKAGE_NAME,
+ /* displayOnRemoteDevices */ true,
+ /* targetDisplayCategory */ null);
+ assertThat(gwpc.canActivityBeLaunched(activityInfo, null,
+ WindowConfiguration.WINDOWING_MODE_FULLSCREEN, DISPLAY_ID_1, /*isNewTask=*/ false))
+ .isTrue();
+ Intent blockedAppIntent = BlockedAppStreamingActivity.createIntent(
+ activityInfo, mAssociationInfo.getDisplayName());
+ verify(mContext, never()).startActivityAsUser(argThat(intent ->
+ intent.filterEquals(blockedAppIntent)), any(), any());
+ }
+
+ @Test
public void registerRunningAppsChangedListener_onRunningAppsChanged_listenersNotified() {
ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index bdbfb7a..17367ef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -125,6 +125,7 @@
import android.app.servertransaction.ClientTransaction;
import android.app.servertransaction.DestroyActivityItem;
import android.app.servertransaction.PauseActivityItem;
+import android.app.servertransaction.WindowStateResizeItem;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -3341,6 +3342,7 @@
// Simulate switching to app2 to make it visible to be IME targets.
spyOn(app2);
spyOn(app2.mClient);
+ spyOn(app2.getProcess());
ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class);
doReturn(true).when(app2).isReadyToDispatchInsetsState();
mDisplayContent.setImeLayeringTarget(app2);
@@ -3351,9 +3353,15 @@
// Verify after unfreezing app2's IME insets state, we won't dispatch visible IME insets
// to client if the app didn't request IME visible.
assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
- verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
- insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
- anyBoolean());
+
+ if (mWm.mFlags.mWindowStateResizeItemFlag) {
+ verify(app2.getProcess()).scheduleClientTransactionItem(
+ isA(WindowStateResizeItem.class));
+ } else {
+ verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),
+ insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(),
+ anyBoolean());
+ }
assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime()));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index baf2594..1aa34ee 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1439,6 +1439,7 @@
activity1.setVisibleRequested(true);
activity1.setVisible(true);
activity2.setVisibleRequested(false);
+ activity1.setState(ActivityRecord.State.RESUMED, "test");
// Using abort to force-finish the sync (since we can't wait for drawing in unit test).
// We didn't call abort on the transition itself, so it will still run onTransactionReady
@@ -1517,6 +1518,8 @@
// Make sure activity1 visibility was committed
assertFalse(activity1.isVisible());
assertFalse(activity1.app.hasActivityInVisibleTask());
+ // Make sure the userLeaving is true and the resuming activity is given,
+ verify(task1).startPausing(eq(true), anyBoolean(), eq(activity2), any());
verify(taskSnapshotController, times(1)).recordSnapshot(eq(task1));
assertTrue(enteringAnimReports.contains(activity2));
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 359ef83..cb37821 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -117,6 +117,16 @@
return buildTestConfig(UNDERLYING_NETWORK_TEMPLATES);
}
+ // Public for use in VcnGatewayConnectionTest
+ public static VcnGatewayConnectionConfig.Builder newTestBuilderMinimal() {
+ final VcnGatewayConnectionConfig.Builder builder = newBuilder();
+ for (int caps : EXPOSED_CAPS) {
+ builder.addExposedCapability(caps);
+ }
+
+ return builder;
+ }
+
private static VcnGatewayConnectionConfig.Builder newBuilder() {
// Append a unique identifier to the name prefix to guarantee that all created
// VcnGatewayConnectionConfigs have a unique name (required by VcnConfig).
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 302af52..bf73198 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
@@ -75,6 +75,9 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionCallback;
+import com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration;
+import com.android.server.vcn.VcnGatewayConnection.VcnIkeSession;
+import com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent;
import com.android.server.vcn.routeselection.UnderlyingNetworkRecord;
import com.android.server.vcn.util.MtuUtils;
@@ -651,6 +654,74 @@
verifySafeModeStateAndCallbackFired(2 /* invocationCount */, true /* isInSafeMode */);
}
+ private void verifySetSafeModeAlarm(
+ boolean safeModeEnabledByCaller,
+ boolean safeModeConfigFlagEnabled,
+ boolean expectingSafeModeEnabled)
+ throws Exception {
+ final VcnGatewayConnectionConfig config =
+ VcnGatewayConnectionConfigTest.newTestBuilderMinimal()
+ .enableSafeMode(safeModeEnabledByCaller)
+ .build();
+ final VcnGatewayConnection.Dependencies deps =
+ mock(VcnGatewayConnection.Dependencies.class);
+ setUpWakeupMessage(
+ mSafeModeTimeoutAlarm, VcnGatewayConnection.SAFEMODE_TIMEOUT_ALARM, deps);
+ doReturn(safeModeConfigFlagEnabled).when(mFeatureFlags).safeModeConfig();
+
+ final VcnGatewayConnection connection =
+ new VcnGatewayConnection(
+ mVcnContext,
+ TEST_SUB_GRP,
+ TEST_SUBSCRIPTION_SNAPSHOT,
+ config,
+ mGatewayStatusCallback,
+ true /* isMobileDataEnabled */,
+ deps);
+
+ connection.setSafeModeAlarm();
+
+ final int expectedCallCnt = expectingSafeModeEnabled ? 1 : 0;
+ verify(deps, times(expectedCallCnt))
+ .newWakeupMessage(
+ eq(mVcnContext),
+ any(),
+ eq(VcnGatewayConnection.SAFEMODE_TIMEOUT_ALARM),
+ any());
+ }
+
+ @Test
+ public void testSafeModeEnabled_configFlagEnabled() throws Exception {
+ verifySetSafeModeAlarm(
+ true /* safeModeEnabledByCaller */,
+ true /* safeModeConfigFlagEnabled */,
+ true /* expectingSafeModeEnabled */);
+ }
+
+ @Test
+ public void testSafeModeEnabled_configFlagDisabled() throws Exception {
+ verifySetSafeModeAlarm(
+ true /* safeModeEnabledByCaller */,
+ false /* safeModeConfigFlagEnabled */,
+ true /* expectingSafeModeEnabled */);
+ }
+
+ @Test
+ public void testSafeModeDisabled_configFlagEnabled() throws Exception {
+ verifySetSafeModeAlarm(
+ false /* safeModeEnabledByCaller */,
+ true /* safeModeConfigFlagEnabled */,
+ false /* expectingSafeModeEnabled */);
+ }
+
+ @Test
+ public void testSafeModeDisabled_configFlagDisabled() throws Exception {
+ verifySetSafeModeAlarm(
+ false /* safeModeEnabledByCaller */,
+ false /* safeModeConfigFlagEnabled */,
+ true /* expectingSafeModeEnabled */);
+ }
+
private Consumer<VcnNetworkAgent> setupNetworkAndGetUnwantedCallback() {
triggerChildOpened();
mTestLooper.dispatchAll();
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index 5efbf59..edced87 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -53,6 +53,7 @@
import android.net.ipsec.ike.IkeSessionCallback;
import android.net.ipsec.ike.IkeSessionConfiguration;
import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.vcn.FeatureFlags;
import android.net.vcn.VcnGatewayConnectionConfig;
import android.net.vcn.VcnGatewayConnectionConfigTest;
import android.os.ParcelUuid;
@@ -165,6 +166,7 @@
@NonNull protected final Context mContext;
@NonNull protected final TestLooper mTestLooper;
@NonNull protected final VcnNetworkProvider mVcnNetworkProvider;
+ @NonNull protected final FeatureFlags mFeatureFlags;
@NonNull protected final VcnContext mVcnContext;
@NonNull protected final VcnGatewayConnectionConfig mConfig;
@NonNull protected final VcnGatewayStatusCallback mGatewayStatusCallback;
@@ -190,6 +192,7 @@
mContext = mock(Context.class);
mTestLooper = new TestLooper();
mVcnNetworkProvider = mock(VcnNetworkProvider.class);
+ mFeatureFlags = mock(FeatureFlags.class);
mVcnContext = mock(VcnContext.class);
mConfig = VcnGatewayConnectionConfigTest.buildTestConfig();
mGatewayStatusCallback = mock(VcnGatewayStatusCallback.class);
@@ -222,6 +225,7 @@
doReturn(mContext).when(mVcnContext).getContext();
doReturn(mTestLooper.getLooper()).when(mVcnContext).getLooper();
doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider();
+ doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
doReturn(mUnderlyingNetworkController)
.when(mDeps)
@@ -241,8 +245,15 @@
doReturn(ELAPSED_REAL_TIME).when(mDeps).getElapsedRealTime();
}
+ protected void setUpWakeupMessage(
+ @NonNull WakeupMessage msg,
+ @NonNull String cmdName,
+ VcnGatewayConnection.Dependencies deps) {
+ doReturn(msg).when(deps).newWakeupMessage(eq(mVcnContext), any(), eq(cmdName), any());
+ }
+
private void setUpWakeupMessage(@NonNull WakeupMessage msg, @NonNull String cmdName) {
- doReturn(msg).when(mDeps).newWakeupMessage(eq(mVcnContext), any(), eq(cmdName), any());
+ setUpWakeupMessage(msg, cmdName, mDeps);
}
@Before
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 977b276..fff8f78 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -122,7 +122,6 @@
"link/AutoVersioner.cpp",
"link/ManifestFixer.cpp",
"link/NoDefaultResourceRemover.cpp",
- "link/ProductFilter.cpp",
"link/PrivateAttributeMover.cpp",
"link/ReferenceLinker.cpp",
"link/ResourceExcluder.cpp",
@@ -135,6 +134,7 @@
"optimize/ResourceFilter.cpp",
"optimize/Obfuscator.cpp",
"optimize/VersionCollapser.cpp",
+ "process/ProductFilter.cpp",
"process/SymbolTable.cpp",
"split/TableSplitter.cpp",
"text/Printer.cpp",
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index b5c290e..728ba8a 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -45,6 +45,7 @@
#include "io/StringStream.h"
#include "io/Util.h"
#include "io/ZipArchive.h"
+#include "process/ProductFilter.h"
#include "trace/TraceBuffer.h"
#include "util/Files.h"
#include "util/Util.h"
@@ -179,6 +180,15 @@
if (!res_parser.Parse(&xml_parser)) {
return false;
}
+
+ if (options.product_.has_value()) {
+ if (!ProductFilter({*options.product_}, /* remove_default_config_values = */ true)
+ .Consume(context, &table)) {
+ context->GetDiagnostics()->Error(android::DiagMessage(path_data.source)
+ << "failed to filter product");
+ return false;
+ }
+ }
}
if (options.pseudolocalize && translatable_file) {
diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h
index 22890fc..61c5b60 100644
--- a/tools/aapt2/cmd/Compile.h
+++ b/tools/aapt2/cmd/Compile.h
@@ -44,6 +44,7 @@
// See comments on aapt::ResourceParserOptions.
bool preserve_visibility_of_styleables = false;
bool verbose = false;
+ std::optional<std::string> product_;
};
/** Parses flags and compiles resources to be used in linking. */
@@ -87,6 +88,10 @@
"Sets the ratio of resources to generate grammatical gender strings for. The "
"ratio has to be a float number between 0 and 1.",
&options_.pseudo_localize_gender_ratio);
+ AddOptionalFlag("--filter-product",
+ "Leave only resources specific to the given product. All "
+ "other resources (including defaults) are removed.",
+ &options_.product_);
}
int Action(const std::vector<std::string>& args) override;
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index eb4e38c..159c6fd 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -66,6 +66,7 @@
#include "optimize/ResourceDeduper.h"
#include "optimize/VersionCollapser.h"
#include "process/IResourceTableConsumer.h"
+#include "process/ProductFilter.h"
#include "process/SymbolTable.h"
#include "split/TableSplitter.h"
#include "trace/TraceBuffer.h"
@@ -2128,7 +2129,7 @@
<< "can't select products when building static library");
}
} else {
- ProductFilter product_filter(options_.products);
+ ProductFilter product_filter(options_.products, /* remove_default_config_values = */ false);
if (!product_filter.Consume(context_, &final_table_)) {
context_->GetDiagnostics()->Error(android::DiagMessage() << "failed stripping products");
return 1;
diff --git a/tools/aapt2/link/Linkers.h b/tools/aapt2/link/Linkers.h
index 44cd276..18165f7 100644
--- a/tools/aapt2/link/Linkers.h
+++ b/tools/aapt2/link/Linkers.h
@@ -20,12 +20,12 @@
#include <set>
#include <unordered_set>
-#include "android-base/macros.h"
-#include "androidfw/ConfigDescription.h"
-#include "androidfw/StringPiece.h"
-
#include "Resource.h"
#include "SdkConstants.h"
+#include "android-base/macros.h"
+#include "android-base/result.h"
+#include "androidfw/ConfigDescription.h"
+#include "androidfw/StringPiece.h"
#include "process/IResourceTableConsumer.h"
#include "xml/XmlDom.h"
@@ -92,28 +92,6 @@
DISALLOW_COPY_AND_ASSIGN(PrivateAttributeMover);
};
-class ResourceConfigValue;
-
-class ProductFilter : public IResourceTableConsumer {
- public:
- using ResourceConfigValueIter = std::vector<std::unique_ptr<ResourceConfigValue>>::iterator;
-
- explicit ProductFilter(std::unordered_set<std::string> products) : products_(products) {
- }
-
- ResourceConfigValueIter SelectProductToKeep(const ResourceNameRef& name,
- const ResourceConfigValueIter begin,
- const ResourceConfigValueIter end,
- android::IDiagnostics* diag);
-
- bool Consume(IAaptContext* context, ResourceTable* table) override;
-
- private:
- DISALLOW_COPY_AND_ASSIGN(ProductFilter);
-
- std::unordered_set<std::string> products_;
-};
-
// Removes namespace nodes and URI information from the XmlResource.
//
// Once an XmlResource is processed by this consumer, it is no longer able to have its attributes
diff --git a/tools/aapt2/link/ProductFilter_test.cpp b/tools/aapt2/link/ProductFilter_test.cpp
deleted file mode 100644
index 2cb9afa..0000000
--- a/tools/aapt2/link/ProductFilter_test.cpp
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "link/Linkers.h"
-
-#include "test/Test.h"
-
-using ::android::ConfigDescription;
-
-namespace aapt {
-
-TEST(ProductFilterTest, SelectTwoProducts) {
- std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
-
- const ConfigDescription land = test::ParseConfigOrDie("land");
- const ConfigDescription port = test::ParseConfigOrDie("port");
-
- ResourceTable table;
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(),
- land)
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/tablet.xml")).Build(),
- land, "tablet")
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(),
- port)
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/tablet.xml")).Build(),
- port, "tablet")
- .Build(),
- context->GetDiagnostics()));
-
- ProductFilter filter({"tablet"});
- ASSERT_TRUE(filter.Consume(context.get(), &table));
-
- EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", land, ""));
- EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", land, "tablet"));
- EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", port, ""));
- EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one", port, "tablet"));
-}
-
-TEST(ProductFilterTest, SelectDefaultProduct) {
- std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
-
- ResourceTable table;
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build())
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("tablet.xml")).Build(), {},
- "tablet")
- .Build(),
- context->GetDiagnostics()));
- ;
-
- ProductFilter filter(std::unordered_set<std::string>{});
- ASSERT_TRUE(filter.Consume(context.get(), &table));
-
- EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one",
- ConfigDescription::DefaultConfig(), ""));
- EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(
- &table, "android:string/one",
- ConfigDescription::DefaultConfig(), "tablet"));
-}
-
-TEST(ProductFilterTest, FailOnAmbiguousProduct) {
- std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
-
- ResourceTable table;
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build())
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("tablet.xml")).Build(), {},
- "tablet")
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("no-sdcard.xml")).Build(),
- {}, "no-sdcard")
- .Build(),
- context->GetDiagnostics()));
-
- ProductFilter filter({"tablet", "no-sdcard"});
- ASSERT_FALSE(filter.Consume(context.get(), &table));
-}
-
-TEST(ProductFilterTest, FailOnMultipleDefaults) {
- std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
-
- ResourceTable table;
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source(".xml")).Build())
- .Build(),
- context->GetDiagnostics()));
-
- ASSERT_TRUE(table.AddResource(
- NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
- .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build(), {},
- "default")
- .Build(),
- context->GetDiagnostics()));
-
- ProductFilter filter(std::unordered_set<std::string>{});
- ASSERT_FALSE(filter.Consume(context.get(), &table));
-}
-
-} // namespace aapt
diff --git a/tools/aapt2/link/ProductFilter.cpp b/tools/aapt2/process/ProductFilter.cpp
similarity index 60%
rename from tools/aapt2/link/ProductFilter.cpp
rename to tools/aapt2/process/ProductFilter.cpp
index 9544986..0b1c0a6 100644
--- a/tools/aapt2/link/ProductFilter.cpp
+++ b/tools/aapt2/process/ProductFilter.cpp
@@ -14,16 +14,18 @@
* limitations under the License.
*/
-#include "link/Linkers.h"
+#include "process/ProductFilter.h"
+
+#include <algorithm>
#include "ResourceTable.h"
#include "trace/TraceBuffer.h"
namespace aapt {
-ProductFilter::ResourceConfigValueIter ProductFilter::SelectProductToKeep(
- const ResourceNameRef& name, const ResourceConfigValueIter begin,
- const ResourceConfigValueIter end, android::IDiagnostics* diag) {
+std::optional<ProductFilter::ResourceConfigValueIter> ProductFilter::SelectProductToKeep(
+ const ResourceNameRef& name, ResourceConfigValueIter begin, ResourceConfigValueIter end,
+ android::IDiagnostics* diag) {
ResourceConfigValueIter default_product_iter = end;
ResourceConfigValueIter selected_product_iter = end;
@@ -36,12 +38,11 @@
<< "selection of product '" << config_value->product << "' for resource "
<< name << " is ambiguous");
- ResourceConfigValue* previously_selected_config_value =
- selected_product_iter->get();
+ ResourceConfigValue* previously_selected_config_value = selected_product_iter->get();
diag->Note(android::DiagMessage(previously_selected_config_value->value->GetSource())
<< "product '" << previously_selected_config_value->product
<< "' is also a candidate");
- return end;
+ return std::nullopt;
}
// Select this product.
@@ -54,11 +55,10 @@
diag->Error(android::DiagMessage(config_value->value->GetSource())
<< "multiple default products defined for resource " << name);
- ResourceConfigValue* previously_default_config_value =
- default_product_iter->get();
+ ResourceConfigValue* previously_default_config_value = default_product_iter->get();
diag->Note(android::DiagMessage(previously_default_config_value->value->GetSource())
<< "default product also defined here");
- return end;
+ return std::nullopt;
}
// Mark the default.
@@ -66,9 +66,16 @@
}
}
+ if (remove_default_config_values_) {
+ // If we are leaving only a specific product, return early here instead of selecting the default
+ // value. Returning end here will cause this value set to be skipped, and will be removed with
+ // ClearEmptyValues method.
+ return selected_product_iter;
+ }
+
if (default_product_iter == end) {
diag->Error(android::DiagMessage() << "no default product defined for resource " << name);
- return end;
+ return std::nullopt;
}
if (selected_product_iter == end) {
@@ -89,20 +96,27 @@
ResourceConfigValueIter start_range_iter = iter;
while (iter != entry->values.end()) {
++iter;
- if (iter == entry->values.end() ||
- (*iter)->config != (*start_range_iter)->config) {
+ if (iter == entry->values.end() || (*iter)->config != (*start_range_iter)->config) {
// End of the array, or we saw a different config,
// so this must be the end of a range of products.
// Select the product to keep from the set of products defined.
ResourceNameRef name(pkg->name, type->named_type, entry->name);
- auto value_to_keep = SelectProductToKeep(
- name, start_range_iter, iter, context->GetDiagnostics());
- if (value_to_keep == iter) {
+ auto value_to_keep =
+ SelectProductToKeep(name, start_range_iter, iter, context->GetDiagnostics());
+ if (!value_to_keep.has_value()) {
// An error occurred, we could not pick a product.
error = true;
- } else {
+ } else if (auto val = value_to_keep.value(); val != iter) {
// We selected a product to keep. Move it to the new array.
- new_values.push_back(std::move(*value_to_keep));
+ if (remove_default_config_values_) {
+ // We are filtering values with the given product. The selected value here will be
+ // a new default value, and all other values will be removed.
+ new_values.push_back(
+ std::make_unique<ResourceConfigValue>((*val)->config, android::StringPiece{}));
+ new_values.back()->value = std::move((*val)->value);
+ } else {
+ new_values.push_back(std::move(*val));
+ }
}
// Start the next range of products.
@@ -115,7 +129,27 @@
}
}
}
+
+ if (remove_default_config_values_) {
+ ClearEmptyValues(table);
+ }
+
return !error;
}
+void ProductFilter::ClearEmptyValues(ResourceTable* table) {
+ // Clear any empty packages/types/entries, as remove_default_config_values_ may remove an entire
+ // value set.
+ CHECK(remove_default_config_values_)
+ << __func__ << " should only be called when remove_default_config_values_ is set";
+
+ for (auto& pkg : table->packages) {
+ for (auto& type : pkg->types) {
+ std::erase_if(type->entries, [](auto& entry) { return entry->values.empty(); });
+ }
+ std::erase_if(pkg->types, [](auto& type) { return type->entries.empty(); });
+ }
+ std::erase_if(table->packages, [](auto& package) { return package->types.empty(); });
+}
+
} // namespace aapt
diff --git a/tools/aapt2/process/ProductFilter.h b/tools/aapt2/process/ProductFilter.h
new file mode 100644
index 0000000..0ec2f00
--- /dev/null
+++ b/tools/aapt2/process/ProductFilter.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "Resource.h"
+#include "android-base/macros.h"
+#include "androidfw/ConfigDescription.h"
+#include "androidfw/IDiagnostics.h"
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+class ResourceConfigValue;
+
+class ProductFilter : public IResourceTableConsumer {
+ public:
+ using ResourceConfigValueIter = std::vector<std::unique_ptr<ResourceConfigValue>>::iterator;
+
+ // Setting remove_default_config_values will remove all values other than
+ // specified product, including default. For example, if the following table
+ //
+ // <string name="foo" product="default">foo_default</string>
+ // <string name="foo" product="tablet">foo_tablet</string>
+ // <string name="bar">bar</string>
+ //
+ // is consumed with tablet, it will result in
+ //
+ // <string name="foo">foo_tablet</string>
+ //
+ // removing foo_default and bar. This option is to generate an RRO package
+ // with given product.
+ explicit ProductFilter(std::unordered_set<std::string> products,
+ bool remove_default_config_values)
+ : products_(std::move(products)),
+ remove_default_config_values_(remove_default_config_values) {
+ }
+
+ bool Consume(IAaptContext* context, ResourceTable* table) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(ProductFilter);
+
+ // SelectProductToKeep returns an iterator for the selected value.
+ //
+ // Returns std::nullopt in case of failure (e.g. ambiguous values, missing or duplicated default
+ // values).
+ // Returns `end` if keep_as_default_product is set and no value for the specified product was
+ // found.
+ std::optional<ResourceConfigValueIter> SelectProductToKeep(const ResourceNameRef& name,
+ ResourceConfigValueIter begin,
+ ResourceConfigValueIter end,
+ android::IDiagnostics* diag);
+
+ void ClearEmptyValues(ResourceTable* table);
+
+ std::unordered_set<std::string> products_;
+ bool remove_default_config_values_;
+};
+
+} // namespace aapt
diff --git a/tools/aapt2/process/ProductFilter_test.cpp b/tools/aapt2/process/ProductFilter_test.cpp
new file mode 100644
index 0000000..27a82dc
--- /dev/null
+++ b/tools/aapt2/process/ProductFilter_test.cpp
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "process/ProductFilter.h"
+
+#include "test/Test.h"
+
+using ::android::ConfigDescription;
+
+namespace aapt {
+
+TEST(ProductFilterTest, SelectTwoProducts) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ const ConfigDescription land = test::ParseConfigOrDie("land");
+ const ConfigDescription port = test::ParseConfigOrDie("port");
+
+ ResourceTable table;
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(),
+ land)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/tablet.xml")).Build(),
+ land, "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(),
+ port)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/tablet.xml")).Build(),
+ port, "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ProductFilter filter({"tablet"}, /* remove_default_config_values = */ false);
+ ASSERT_TRUE(filter.Consume(context.get(), &table));
+
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, ""));
+ EXPECT_NE(nullptr,
+ test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, "tablet"));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, ""));
+ EXPECT_NE(nullptr,
+ test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, "tablet"));
+}
+
+TEST(ProductFilterTest, SelectDefaultProduct) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ ResourceTable table;
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build())
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("tablet.xml")).Build(), {},
+ "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+ ;
+
+ ProductFilter filter(std::unordered_set<std::string>{},
+ /* remove_default_config_values = */ false);
+ ASSERT_TRUE(filter.Consume(context.get(), &table));
+
+ EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one",
+ ConfigDescription::DefaultConfig(), ""));
+ EXPECT_EQ(nullptr,
+ test::GetValueForConfigAndProduct<Id>(&table, "android:string/one",
+ ConfigDescription::DefaultConfig(), "tablet"));
+}
+
+TEST(ProductFilterTest, FailOnAmbiguousProduct) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ ResourceTable table;
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build())
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("tablet.xml")).Build(), {},
+ "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("no-sdcard.xml")).Build(),
+ {}, "no-sdcard")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ProductFilter filter({"tablet", "no-sdcard"}, /* remove_default_config_values = */ false);
+ ASSERT_FALSE(filter.Consume(context.get(), &table));
+}
+
+TEST(ProductFilterTest, FailOnMultipleDefaults) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ ResourceTable table;
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source(".xml")).Build())
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("default.xml")).Build(), {},
+ "default")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ProductFilter filter(std::unordered_set<std::string>{},
+ /* remove_default_config_values = */ false);
+ ASSERT_FALSE(filter.Consume(context.get(), &table));
+}
+
+TEST(ProductFilterTest, RemoveDefaultConfigValues) {
+ std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+ const ConfigDescription land = test::ParseConfigOrDie("land");
+ const ConfigDescription port = test::ParseConfigOrDie("port");
+
+ ResourceTable table;
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(),
+ land)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/tablet.xml")).Build(),
+ land, "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/two"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("land/default.xml")).Build(),
+ land)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(),
+ port)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/one"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/tablet.xml")).Build(),
+ port, "tablet")
+ .Build(),
+ context->GetDiagnostics()));
+
+ ASSERT_TRUE(table.AddResource(
+ NewResourceBuilder(test::ParseNameOrDie("android:string/two"))
+ .SetValue(test::ValueBuilder<Id>().SetSource(android::Source("port/default.xml")).Build(),
+ port)
+ .Build(),
+ context->GetDiagnostics()));
+
+ ProductFilter filter({"tablet"}, /* remove_default_config_values = */ true);
+ ASSERT_TRUE(filter.Consume(context.get(), &table));
+
+ EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", land, ""));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/two", land, ""));
+ EXPECT_NE(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/one", port, ""));
+ EXPECT_EQ(nullptr, test::GetValueForConfigAndProduct<Id>(&table, "android:string/two", port, ""));
+}
+
+} // namespace aapt